Merge "Enable disabled TypeAdapterStoreTest." into androidx-main
diff --git a/activity/activity-compose-lint/build.gradle b/activity/activity-compose-lint/build.gradle
index 42e9863..f60b53f 100644
--- a/activity/activity-compose-lint/build.gradle
+++ b/activity/activity-compose-lint/build.gradle
@@ -34,13 +34,13 @@
dependencies {
compileOnly(libs.androidLintMinApi)
compileOnly(libs.kotlinStdlib)
- bundleInside(projectOrArtifact(":compose:lint:common"))
+ bundleInside(project(":compose:lint:common"))
compileOnly(libs.intellijCore)
compileOnly(libs.uast)
compileOnly(libs.intellijKotlinCompiler)
- testImplementation(projectOrArtifact(":compose:lint:common-test"))
+ testImplementation(project(":compose:lint:common-test"))
testImplementation(libs.kotlinStdlib)
testRuntimeOnly(libs.kotlinReflect)
testImplementation(libs.kotlinStdlibJdk8)
diff --git a/activity/activity-compose/build.gradle b/activity/activity-compose/build.gradle
index 1ec1fad..bd7ba5b 100644
--- a/activity/activity-compose/build.gradle
+++ b/activity/activity-compose/build.gradle
@@ -36,7 +36,7 @@
implementation(libs.kotlinCoroutinesCore)
api("androidx.compose.runtime:runtime:1.0.1")
api("androidx.compose.runtime:runtime-saveable:1.0.1")
- api(projectOrArtifact(":activity:activity-ktx"))
+ api(project(":activity:activity-ktx"))
api("androidx.compose.ui:ui:1.0.1")
api("androidx.core:core-ktx:1.13.0")
api("androidx.lifecycle:lifecycle-viewmodel:2.6.1")
@@ -46,8 +46,8 @@
androidTestImplementation("androidx.annotation:annotation:1.8.1")
androidTestImplementation("androidx.compose.foundation:foundation-layout:1.6.0")
- androidTestImplementation projectOrArtifact(":compose:ui:ui-test-junit4")
- androidTestImplementation projectOrArtifact(":compose:material:material")
+ androidTestImplementation project(":compose:ui:ui-test-junit4")
+ androidTestImplementation project(":compose:material:material")
androidTestRuntimeOnly project(":compose:test-utils")
androidTestImplementation(project(":compose:foundation:foundation"))
androidTestImplementation(project(":compose:runtime:runtime"))
@@ -57,7 +57,7 @@
androidTestImplementation(project(":compose:ui:ui-text"))
androidTestImplementation(project(":lifecycle:lifecycle-common"))
androidTestImplementation(project(":lifecycle:lifecycle-runtime"))
- androidTestImplementation projectOrArtifact(":lifecycle:lifecycle-runtime-testing")
+ androidTestImplementation project(":lifecycle:lifecycle-runtime-testing")
androidTestImplementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.2")
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
@@ -65,8 +65,8 @@
androidTestImplementation(libs.junit)
androidTestImplementation(libs.truth)
- lintChecks(projectOrArtifact(":activity:activity-compose-lint"))
- lintPublish(projectOrArtifact(":activity:activity-compose-lint"))
+ lintChecks(project(":activity:activity-compose-lint"))
+ lintPublish(project(":activity:activity-compose-lint"))
}
androidx {
@@ -75,7 +75,7 @@
inceptionYear = "2020"
description = "Compose integration with Activity"
legacyDisableKotlinStrictApiMode = true
- samples(projectOrArtifact(":activity:activity-compose:activity-compose-samples"))
+ samples(project(":activity:activity-compose:activity-compose-samples"))
}
android {
diff --git a/activity/activity-compose/samples/build.gradle b/activity/activity-compose/samples/build.gradle
index 27063bd..361a6fa 100644
--- a/activity/activity-compose/samples/build.gradle
+++ b/activity/activity-compose/samples/build.gradle
@@ -38,8 +38,8 @@
api("androidx.compose.foundation:foundation-layout:1.0.1")
api("androidx.compose.runtime:runtime:1.0.1")
implementation "androidx.compose.foundation:foundation:1.0.1"
- implementation projectOrArtifact(":activity:activity-compose")
- implementation projectOrArtifact(":activity:activity")
+ implementation project(":activity:activity-compose")
+ implementation project(":activity:activity")
implementation("androidx.compose.ui:ui-graphics:1.0.1")
implementation("androidx.compose.ui:ui-text:1.0.1")
implementation("androidx.compose.ui:ui:1.0.1")
diff --git a/activity/integration-tests/baselineprofile/build.gradle b/activity/integration-tests/baselineprofile/build.gradle
index b340454..1d63008 100644
--- a/activity/integration-tests/baselineprofile/build.gradle
+++ b/activity/integration-tests/baselineprofile/build.gradle
@@ -47,8 +47,8 @@
}
dependencies {
- implementation(projectOrArtifact(":benchmark:benchmark-junit4"))
- implementation(projectOrArtifact(":benchmark:benchmark-macro-junit4"))
+ implementation(project(":benchmark:benchmark-junit4"))
+ implementation(project(":benchmark:benchmark-macro-junit4"))
implementation(libs.testRules)
implementation(libs.testExtJunit)
implementation(libs.testCore)
diff --git a/annotation/annotation/build.gradle b/annotation/annotation/build.gradle
index 0b88e7c..2f5d5d7 100644
--- a/annotation/annotation/build.gradle
+++ b/annotation/annotation/build.gradle
@@ -42,7 +42,7 @@
wasmJsMain {
dependsOn(nonJvmMain)
dependencies {
- api(libs.kotlinStdlibJs)
+ implementation(libs.kotlinStdlibJs)
}
}
diff --git a/appcompat/appcompat-lint/integration-tests/build.gradle b/appcompat/appcompat-lint/integration-tests/build.gradle
index 4c2adc6..5153ca8 100644
--- a/appcompat/appcompat-lint/integration-tests/build.gradle
+++ b/appcompat/appcompat-lint/integration-tests/build.gradle
@@ -13,7 +13,7 @@
dependencies {
implementation(project(":appcompat:appcompat"))
- implementation(projectOrArtifact(":core:core"))
+ implementation(project(":core:core"))
api(libs.kotlinStdlib)
}
diff --git a/appcompat/appcompat-resources/build.gradle b/appcompat/appcompat-resources/build.gradle
index a34ebd8..19f68bf 100644
--- a/appcompat/appcompat-resources/build.gradle
+++ b/appcompat/appcompat-resources/build.gradle
@@ -43,8 +43,8 @@
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.truth)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(project(":internal-testutils-runtime"), {
exclude group: "androidx.appcompat", module: "appcompat-resources"
})
diff --git a/appcompat/appcompat/build.gradle b/appcompat/appcompat/build.gradle
index fd5e26f..f4fc9020 100644
--- a/appcompat/appcompat/build.gradle
+++ b/appcompat/appcompat/build.gradle
@@ -46,8 +46,8 @@
androidTestImplementation(libs.testUiautomator)
androidTestImplementation(libs.truth)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it's own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it's own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation("androidx.lifecycle:lifecycle-viewmodel:2.6.1")
androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.6.1", {
// Needed to ensure that the same version of lifecycle-runtime-ktx
diff --git a/appcompat/integration-tests/receive-content-testapp/build.gradle b/appcompat/integration-tests/receive-content-testapp/build.gradle
index c33e1bf..3bb6c17 100644
--- a/appcompat/integration-tests/receive-content-testapp/build.gradle
+++ b/appcompat/integration-tests/receive-content-testapp/build.gradle
@@ -29,7 +29,7 @@
implementation(project(":appcompat:appcompat"))
implementation(libs.constraintLayout)
implementation(libs.guavaAndroid)
- implementation(projectOrArtifact(":recyclerview:recyclerview"))
+ implementation(project(":recyclerview:recyclerview"))
implementation(libs.material)
androidTestImplementation("androidx.lifecycle:lifecycle-common:2.6.1")
diff --git a/appsearch/appsearch/api/current.txt b/appsearch/appsearch/api/current.txt
index 0cc59d0..9764ce3 100644
--- a/appsearch/appsearch/api/current.txt
+++ b/appsearch/appsearch/api/current.txt
@@ -291,6 +291,9 @@
method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
}
+ @SuppressCompatibility @RequiresOptIn(level=androidx.annotation.RequiresOptIn.Level.ERROR) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalAppSearchApi {
+ }
+
public interface Features {
method public int getMaxIndexedProperties();
method public boolean isFeatureSupported(String);
diff --git a/appsearch/appsearch/api/restricted_current.txt b/appsearch/appsearch/api/restricted_current.txt
index 0cc59d0..9764ce3 100644
--- a/appsearch/appsearch/api/restricted_current.txt
+++ b/appsearch/appsearch/api/restricted_current.txt
@@ -291,6 +291,9 @@
method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
}
+ @SuppressCompatibility @RequiresOptIn(level=androidx.annotation.RequiresOptIn.Level.ERROR) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalAppSearchApi {
+ }
+
public interface Features {
method public int getMaxIndexedProperties();
method public boolean isFeatureSupported(String);
diff --git a/appsearch/appsearch/build.gradle b/appsearch/appsearch/build.gradle
index 2a7270f..79542c5 100644
--- a/appsearch/appsearch/build.gradle
+++ b/appsearch/appsearch/build.gradle
@@ -43,7 +43,7 @@
implementation("androidx.collection:collection:1.4.2")
implementation('androidx.concurrent:concurrent-futures:1.0.0')
- implementation("androidx.core:core:1.6.0")
+ implementation("androidx.core:core:1.9.0")
annotationProcessor project(':appsearch:appsearch-compiler')
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/ExperimentalAppSearchApi.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/ExperimentalAppSearchApi.java
new file mode 100644
index 0000000..3595a7d
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/ExperimentalAppSearchApi.java
@@ -0,0 +1,23 @@
+/*
+ * 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.appsearch.app;
+
+import androidx.annotation.RequiresOptIn;
+
+/** Indicates that an AppSearch api is unstable. */
+@RequiresOptIn(level = RequiresOptIn.Level.ERROR)
+public @interface ExperimentalAppSearchApi {}
+
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
index 006d119..28e9fa2 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
@@ -36,6 +36,7 @@
import androidx.appsearch.safeparcel.GenericDocumentParcel;
import androidx.appsearch.safeparcel.PropertyParcel;
import androidx.appsearch.util.IndentingStringBuilder;
+import androidx.core.os.ParcelCompat;
import androidx.core.util.Preconditions;
import java.lang.reflect.Array;
@@ -190,9 +191,11 @@
@NonNull
public static GenericDocument createFromParcel(@NonNull Parcel parcel) {
Objects.requireNonNull(parcel);
- return new GenericDocument(
- parcel.readParcelable(
- GenericDocumentParcel.class.getClassLoader(), GenericDocumentParcel.class));
+ GenericDocumentParcel documentParcel =
+ ParcelCompat.readParcelable(
+ parcel, GenericDocumentParcel.class.getClassLoader(),
+ GenericDocumentParcel.class);
+ return new GenericDocument(documentParcel);
}
/**
diff --git a/appsearch/exportToFramework.py b/appsearch/exportToFramework.py
index 373cd50..ea807db 100755
--- a/appsearch/exportToFramework.py
+++ b/appsearch/exportToFramework.py
@@ -179,6 +179,9 @@
.replace(
'androidx.core.util.ObjectsCompat',
'java.util.Objects')
+ .replace(
+ 'import androidx.core.os.ParcelCompat',
+ 'import android.core.os.Parcel')
# Preconditions.checkNotNull is replaced with Objects.requireNonNull. We add both
# imports and let google-java-format sort out which one is unused.
.replace(
@@ -196,6 +199,9 @@
contents = re.sub(r'\/\/ @exportToFramework:copyToPath\([^)]+\)', '', contents)
contents = re.sub(r'@RequiresFeature\([^)]*\)', '', contents, flags=re.DOTALL)
+ contents = re.sub(r'ParcelCompat\.readParcelable\(.*?([a-zA-Z]+), ', r'\1.readParcelable(',
+ contents, flags=re.DOTALL)
+
# Jetpack methods have the Async suffix, but framework doesn't. Strip the Async suffix
# to allow the same documentation to compile for both.
contents = re.sub(r'(#[a-zA-Z0-9_]+)Async}', r'\1}', contents)
diff --git a/arch/core/core-testing/build.gradle b/arch/core/core-testing/build.gradle
index dda5131..f5688ed 100644
--- a/arch/core/core-testing/build.gradle
+++ b/arch/core/core-testing/build.gradle
@@ -32,7 +32,7 @@
api(project(":arch:core:core-runtime"))
api("androidx.annotation:annotation:1.8.1")
api(libs.junit)
- api(libs.mockitoCore, excludes.bytebuddy)
+ api(libs.mockitoCore)
testImplementation(libs.junit)
diff --git a/autofill/autofill/src/main/java/androidx/autofill/HintConstants.java b/autofill/autofill/src/main/java/androidx/autofill/HintConstants.java
index 12612a1..028e2dc 100644
--- a/autofill/autofill/src/main/java/androidx/autofill/HintConstants.java
+++ b/autofill/autofill/src/main/java/androidx/autofill/HintConstants.java
@@ -40,7 +40,7 @@
* should be <code>{@value #AUTOFILL_HINT_EMAIL_ADDRESS}</code>).
*
* <p>See {@link android.view.View#setAutofillHints(String...)} for more info about autofill
- * hints.3
+ * hints.
*/
public static final String AUTOFILL_HINT_EMAIL_ADDRESS = "emailAddress";
diff --git a/benchmark/baseline-profile-gradle-plugin/lint-baseline.xml b/benchmark/baseline-profile-gradle-plugin/lint-baseline.xml
index 65975e0..d8fe4cd 100644
--- a/benchmark/baseline-profile-gradle-plugin/lint-baseline.xml
+++ b/benchmark/baseline-profile-gradle-plugin/lint-baseline.xml
@@ -4,69 +4,6 @@
<issue
id="GradleProjectIsolation"
message="Use providers.gradleProperty instead of getProperties"
- errorLine1=" project.properties["androidx.benchmark.test.maxagpversion"]?.let { str ->"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of getProperties"
- errorLine1=" it in project.properties && project.properties[it].toString().toBoolean()"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of getProperties"
- errorLine1=" it in project.properties && project.properties[it].toString().toBoolean()"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of getProperties"
- errorLine1=" project.properties.containsKey(PROP_SKIP_GENERATION)"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of getProperties"
- errorLine1=" project.properties.containsKey(PROP_FORCE_ONLY_CONNECTED_DEVICES)"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of getProperties"
- errorLine1=" !project.properties.containsKey(PROP_DONT_DISABLE_RULES)"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of getProperties"
- errorLine1=" !project.properties.containsKey(PROP_SEND_TARGET_PACKAGE_NAME)"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of getProperties"
errorLine1=" project.properties.filterKeys { k ->"
errorLine2=" ~~~~~~~~~~">
<location
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt
index 239eff6..aa184ec 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt
@@ -79,16 +79,16 @@
private val baselineProfileExtension = BaselineProfileProducerExtension.register(project)
private val configurationManager = ConfigurationManager(project)
private val shouldSkipGeneration by lazy {
- project.properties.containsKey(PROP_SKIP_GENERATION)
+ project.providers.gradleProperty(PROP_SKIP_GENERATION).isPresent
}
private val forceOnlyConnectedDevices: Boolean by lazy {
- project.properties.containsKey(PROP_FORCE_ONLY_CONNECTED_DEVICES)
+ project.providers.gradleProperty(PROP_FORCE_ONLY_CONNECTED_DEVICES).isPresent
}
private val addEnabledRulesInstrumentationArgument by lazy {
- !project.properties.containsKey(PROP_DONT_DISABLE_RULES)
+ !project.providers.gradleProperty(PROP_DONT_DISABLE_RULES).isPresent
}
private val addTargetPackageNameInstrumentationArgument by lazy {
- !project.properties.containsKey(PROP_SEND_TARGET_PACKAGE_NAME)
+ !project.providers.gradleProperty(PROP_SEND_TARGET_PACKAGE_NAME).isPresent
}
// This maps all the extended build types to the original ones. Note that release does not
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt
index cbd7fa7..29f7c75 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt
@@ -51,8 +51,9 @@
// Properties that can be specified by cmd line using -P<property_name> when invoking gradle.
val testMaxAgpVersion by lazy {
- project.properties["androidx.benchmark.test.maxagpversion"]?.let { str ->
- val parts = str.toString().split(".").map { it.toInt() }
+ project.providers.gradleProperty("androidx.benchmark.test.maxagpversion").orNull?.let { str
+ ->
+ val parts = str.split(".").map { it.toInt() }
return@lazy AndroidPluginVersion(parts[0], parts[1], parts[2])
} ?: return@lazy null
}
diff --git a/benchmark/benchmark-darwin-gradle-plugin/lint-baseline.xml b/benchmark/benchmark-darwin-gradle-plugin/lint-baseline.xml
index 9ce75c2..42b07bb 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/lint-baseline.xml
+++ b/benchmark/benchmark-darwin-gradle-plugin/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.6.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.6.0-beta01)" variant="all" version="8.6.0-beta01">
+<issues format="6" by="lint 8.7.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.7.0-alpha02)" variant="all" version="8.7.0-alpha02">
<issue
id="GradleProjectIsolation"
@@ -11,6 +11,15 @@
</issue>
<issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.projectDir, // frameworks/support"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt"/>
+ </issue>
+
+ <issue
id="WithTypeWithoutConfigureEach"
message="Avoid passing a closure to withType, use withType().configureEach instead"
errorLine1=" project.plugins.withType(KotlinMultiplatformPluginWrapper::class.java) {"
diff --git a/benchmark/benchmark-macro-junit4/build.gradle b/benchmark/benchmark-macro-junit4/build.gradle
index 4f9ef1f..f8663aa 100644
--- a/benchmark/benchmark-macro-junit4/build.gradle
+++ b/benchmark/benchmark-macro-junit4/build.gradle
@@ -54,10 +54,8 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.espressoCore)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
- // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
}
tasks.withType(KotlinCompile).configureEach {
diff --git a/benchmark/gradle-plugin/lint-baseline.xml b/benchmark/gradle-plugin/lint-baseline.xml
index 4363bf5..8c91fdc 100644
--- a/benchmark/gradle-plugin/lint-baseline.xml
+++ b/benchmark/gradle-plugin/lint-baseline.xml
@@ -3,27 +3,36 @@
<issue
id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of findProperty"
- errorLine1=" if (!project.findProperty(ADDITIONAL_TEST_OUTPUT_KEY).toString().toBoolean()) {"
- errorLine2=" ~~~~~~~~~~~~">
+ message="Avoid using method getRootProject"
+ errorLine1=" if (!project.rootProject.tasks.exists("lockClocks")) {"
+ errorLine2=" ~~~~~~~~~~~">
<location
file="src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt"/>
</issue>
<issue
id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of findProperty"
- errorLine1=" project.findProperty("androidx.benchmark.lockClocks.cores")?.toString() ?: """
- errorLine2=" ~~~~~~~~~~~~">
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.tasks.register("lockClocks", LockClocksTask::class.java).configure {"
+ errorLine2=" ~~~~~~~~~~~">
<location
file="src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt"/>
</issue>
<issue
id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of getProperties"
- errorLine1=" if (!project.properties[ADDITIONAL_TEST_OUTPUT_KEY].toString().toBoolean()) {"
- errorLine2=" ~~~~~~~~~~">
+ message="Avoid using method getRootProject"
+ errorLine1=" if (!project.rootProject.tasks.exists("unlockClocks")) {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.tasks"
+ errorLine2=" ~~~~~~~~~~~">
<location
file="src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt"/>
</issue>
diff --git a/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt b/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt
index a7bd8e3..8b2eb89 100644
--- a/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt
+++ b/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt
@@ -101,14 +101,19 @@
extension.buildTypes.named(testBuildType).configure { it.isDefault = true }
if (
- !project.rootProject.hasProperty("android.injected.invoked.from.ide") &&
+ !project.providers.gradleProperty("android.injected.invoked.from.ide").isPresent &&
!testInstrumentationArgs.containsKey("androidx.benchmark.output.enable")
) {
// NOTE: This argument is checked by ResultWriter to enable CI reports.
defaultConfig.testInstrumentationRunnerArguments["androidx.benchmark.output.enable"] =
"true"
- if (!project.findProperty(ADDITIONAL_TEST_OUTPUT_KEY).toString().toBoolean()) {
+ if (
+ !project.providers
+ .gradleProperty(ADDITIONAL_TEST_OUTPUT_KEY)
+ .getOrElse("false")
+ .toBoolean()
+ ) {
defaultConfig.testInstrumentationRunnerArguments["no-isolated-storage"] = "1"
}
}
@@ -119,7 +124,9 @@
project.rootProject.tasks.register("lockClocks", LockClocksTask::class.java).configure {
it.adbPath.set(adbPathProvider)
it.coresArg.set(
- project.findProperty("androidx.benchmark.lockClocks.cores")?.toString() ?: ""
+ project.providers
+ .gradleProperty("androidx.benchmark.lockClocks.cores")
+ .orElse("")
)
}
}
@@ -159,7 +166,12 @@
project.layout.buildDirectory.dir(
"outputs/connected_android_test_additional_output"
)
- if (!project.properties[ADDITIONAL_TEST_OUTPUT_KEY].toString().toBoolean()) {
+ if (
+ !project.providers
+ .gradleProperty(ADDITIONAL_TEST_OUTPUT_KEY)
+ .getOrElse("false")
+ .toBoolean()
+ ) {
// Only enable pulling benchmark data through this plugin on older versions of
// AGP that do not yet enable this flag.
project.tasks
diff --git a/biometric/biometric/build.gradle b/biometric/biometric/build.gradle
index 49370d4..4bd2bd8 100644
--- a/biometric/biometric/build.gradle
+++ b/biometric/biometric/build.gradle
@@ -57,8 +57,8 @@
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testUiautomator)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
+ androidTestImplementation(libs.dexmakerMockito)
+ androidTestImplementation(libs.mockitoCore)
androidTestImplementation(libs.truth)
androidTestImplementation("androidx.fragment:fragment-testing:1.4.1")
}
diff --git a/browser/browser/build.gradle b/browser/browser/build.gradle
index 9af6f1b..7606121 100644
--- a/browser/browser/build.gradle
+++ b/browser/browser/build.gradle
@@ -51,8 +51,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(project(":internal-testutils-runtime"))
}
diff --git a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsService.java b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsService.java
index 4ec3c2a..2ce6b17 100644
--- a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsService.java
+++ b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsService.java
@@ -221,7 +221,7 @@
public void prefetch(@NonNull ICustomTabsCallback callback, @NonNull Uri url,
@NonNull Bundle options) {
CustomTabsService.this.prefetch(
- new CustomTabsSessionToken(callback, null), url,
+ new CustomTabsSessionToken(callback, getSessionIdFromBundle(options)), url,
PrefetchOptions.fromBundle(options));
}
diff --git a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSession.java b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSession.java
index ca297bb..505dffc 100644
--- a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSession.java
+++ b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSession.java
@@ -128,8 +128,9 @@
@ExperimentalPrefetch
@SuppressWarnings("NullAway") // TODO: b/142938599
public void prefetch(@NonNull Uri url, @NonNull PrefetchOptions options) {
+ Bundle optionsWithId = createBundleWithId(options.toBundle());
try {
- mService.prefetch(mCallback, url, options.toBundle());
+ mService.prefetch(mCallback, url, optionsWithId);
} catch (RemoteException e) {
return;
}
@@ -146,9 +147,10 @@
@ExperimentalPrefetch
@SuppressWarnings("NullAway") // TODO: b/142938599
public void prefetch(@NonNull List<Uri> urls, @NonNull PrefetchOptions options) {
+ Bundle optionsWithId = createBundleWithId(options.toBundle());
try {
for (Uri uri : urls) {
- mService.prefetch(mCallback, uri, options.toBundle());
+ mService.prefetch(mCallback, uri, optionsWithId);
}
} catch (RemoteException e) {
return;
diff --git a/buildSrc-tests/lint-baseline.xml b/buildSrc-tests/lint-baseline.xml
index 235650f..8282927 100644
--- a/buildSrc-tests/lint-baseline.xml
+++ b/buildSrc-tests/lint-baseline.xml
@@ -138,6 +138,69 @@
<issue
id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" val extensions = project.rootProject.extensions"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/dependencyTracker/AffectedModuleDetector.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" val compilerProject = project.rootProject.resolveProject(":compose")"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXComposeImplPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.tasks.named(zipComposeMetricsTaskName).configure { zipTask ->"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXComposeImplPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.tasks.named(zipComposeReportsTaskName).configure { zipTask ->"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXComposeImplPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" return project.rootProject.layout.buildDirectory"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXComposeImplPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" return project.rootProject.layout.buildDirectory"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXComposeImplPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" return File(rootProject.projectDir, "../../external").canonicalFile"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/AndroidXConfig.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
message="Use providers.gradleProperty instead of getProperties"
errorLine1=" for (propertyName in project.properties.keys) {"
errorLine2=" ~~~~~~~~~~">
@@ -147,11 +210,137 @@
<issue
id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of getProperties"
- errorLine1=" if (properties.containsKey("android.injected.invoked.from.ide")) {"
- errorLine2=" ~~~~~~~~~~">
+ message="Avoid using method getRootProject"
+ errorLine1=" AndroidXPlaygroundRootImplPlugin.projectOrArtifact(rootProject, this)"
+ errorLine2=" ~~~~~~~~~~~">
<location
- file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXRootImplPlugin.kt"/>
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXImplPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method findProject"
+ errorLine1=" allProjectsExist || findProject(otherGradlePath) != null"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXImplPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.rootDir == project.getSupportRootFolder()"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXImplPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" rootProject.extensions.findByType<NodeJsRootExtension>()?.version = getVersionByName("node")"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXMultiplatformExtension.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" rootProject.extensions.findByType(YarnRootExtension::class.java)?.let {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXMultiplatformExtension.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" rootProject.tasks.register("createYarnRcFile", CreateYarnRcFileTask::class.java) {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXMultiplatformExtension.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" it.yarnrcFile.set(rootProject.layout.buildDirectory.file("js/.yarnrc"))"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXMultiplatformExtension.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" rootProject.tasks.withType<KotlinNpmInstallTask>().configureEach {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXMultiplatformExtension.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method findProject"
+ errorLine1=" val requested = rootProject.findProject(path)"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXPlaygroundRootImplPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.tasks.named(NAME).configure { it.dependsOn(task) }"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXPlaygroundRootImplPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" rootProject.layout.buildDirectory.dir("test-xml-configs")"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/BuildServerConfiguration.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" rootProject.layout.buildDirectory.dir("privacysandbox-files")"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/BuildServerConfiguration.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" val actualRootProject = if (project.isRoot) project else project.rootProject"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/BuildServerConfiguration.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" rootProject.tasks.named(CREATE_AGGREGATE_BUILD_INFO_FILES_TASK).configure {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/buildInfo/CreateAggregateLibraryBuildInfoFileTask.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" get() = this == rootProject"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/gradle/Extensions.kt"/>
</issue>
<issue
@@ -174,6 +363,15 @@
<issue
id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" rootProject.tasks.named(GLOBAL_TASK_NAME).configure { task ->"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/FilteredAnchorTask.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
message="Use providers.gradleProperty instead of getProperties"
errorLine1=" task.pathPrefix = properties[PROP_PATH_PREFIX] as String"
errorLine2=" ~~~~~~~~~~">
@@ -191,6 +389,204 @@
</issue>
<issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" it.parameters.workingDir.set(rootProject.layout.projectDirectory)"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/gitclient/GitClient.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getParent"
+ errorLine1=" ${project.parent}."
+ errorLine2=" ~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/java/JavaCompileInputs.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" val rootBaseDir = if (compilerDaemonDisabled) projectDir else rootProject.projectDir"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/KonanPrebuiltsSetup.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method findProject"
+ errorLine1=" return project.rootProject.findProject(path)"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/LintConfiguration.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" return project.rootProject.findProject(path)"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/LintConfiguration.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method findProject"
+ errorLine1=" project.rootProject.findProject(":lint:lint-gradle")?.let {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/LintConfiguration.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.findProject(":lint:lint-gradle")?.let {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/LintConfiguration.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" val tasksByOutput = project.rootProject.findAllTasksByOutput()"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/ListTaskOutputsTask.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method findProject"
+ errorLine1=" project.findProject(projectPath)?.plugins?.let { plugins ->"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/MavenUploadHelper.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" rootProject.tasks.named(CREATE_MODULE_INFO).configure {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/testConfiguration/OwnersService.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.gradle.sharedServices.registerIfAbsent("
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/ProjectParser.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" regenerate(project.rootProject, groupId, artifactId, artifactVersion, location)"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/metalava/RegenerateOldApisTask.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" regenerate(project.rootProject, groupId, artifactId, version, location)"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/metalava/RegenerateOldApisTask.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.maybeRegister("
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/Release.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.getRepositoryDirectory()"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/Release.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" return project.rootProject.maybeRegister("
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/Release.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" val localPropsFile = rootProject.projectDir.resolve("local.properties")"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/SdkHelper.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.rootDir.toRelativeString(project.projectDir)"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/SdkResourceGenerator.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" (project.rootProject.extensions.extraProperties).let { it.get("supportRootFolder") as File }"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/studio/StudioTask.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" rootProject.tasks"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/testConfiguration/TestSuiteConfiguration.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getParent"
+ errorLine1=" val parentProject = project.parent!!"
+ errorLine2=" ~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/testConfiguration/TestSuiteConfiguration.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.tasks.findByName(FINALIZE_TEST_CONFIGS_WITH_APKS_TASK)!!.dependsOn(task)"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/testConfiguration/TestSuiteConfiguration.kt"/>
+ </issue>
+
+ <issue
id="InternalAgpApiUsage"
message="Avoid using internal Android Gradle Plugin APIs"
errorLine1="import com.android.build.gradle.internal.lint.AndroidLintAnalysisTask"
@@ -382,8 +778,8 @@
<issue
id="WithPluginClasspathUsage"
message="Avoid usage of GradleRunner#withPluginClasspath, which is broken. Instead use something like https://github.com/autonomousapps/dependency-analysis-gradle-plugin/tree/main/testkit#gradle-testkit-support-plugin"
- errorLine1=" .withPluginClasspath()"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ errorLine1=" .withPluginClasspath()"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
<location
file="src/test/java/androidx/build/buildInfo/CreateLibraryBuildInfoFileTaskTest.kt"/>
</issue>
diff --git a/buildSrc/dependencies.gradle b/buildSrc/dependencies.gradle
index 23c0358..48fa25e 100644
--- a/buildSrc/dependencies.gradle
+++ b/buildSrc/dependencies.gradle
@@ -16,10 +16,6 @@
// Add ext.libs for library versions
def excludes = [:]
-excludes.bytebuddy = {
- exclude group: "net.bytebuddy"
-}
-
excludes.espresso = {
exclude group: "androidx.annotation"
exclude group: "androidx.appcompat"
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index cd8fc46..1894b75 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -34,9 +34,8 @@
import androidx.build.sbom.configureSbomPublishing
import androidx.build.sbom.validateAllArchiveInputsRecognized
import androidx.build.studio.StudioTask
-import androidx.build.testConfiguration.ModuleInfoGenerator
-import androidx.build.testConfiguration.TestModule
import androidx.build.testConfiguration.addAppApkToTestConfigGeneration
+import androidx.build.testConfiguration.addToModuleInfo
import androidx.build.testConfiguration.configureTestConfigGeneration
import androidx.build.uptodatedness.TaskUpToDateValidator
import androidx.build.uptodatedness.cacheEvenIfNoOutputs
@@ -306,16 +305,7 @@
val xmlReportDestDir = project.getHostTestResultDirectory()
val testName = "${project.path}:${task.name}"
- project.rootProject.tasks.named("createModuleInfo").configure {
- it as ModuleInfoGenerator
- it.testModules.add(
- TestModule(
- name = testName,
- path =
- listOf(project.projectDir.toRelativeString(project.getSupportRootFolder()))
- )
- )
- }
+ project.addToModuleInfo(testName)
val archiveName = "$testName.zip"
if (project.isDisplayTestOutput()) {
// Enable tracing to see results in command line
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
index e777875..ba8881f 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
@@ -61,7 +61,7 @@
// If we're running inside Studio, validate the Android Gradle Plugin version.
val expectedAgpVersion = System.getenv("EXPECTED_AGP_VERSION")
- if (properties.containsKey("android.injected.invoked.from.ide")) {
+ if (providers.gradleProperty("android.injected.invoked.from.ide").isPresent) {
if (expectedAgpVersion != ANDROID_GRADLE_PLUGIN_VERSION) {
throw GradleException(
"""
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/FilteredAnchorTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/FilteredAnchorTask.kt
index f632090..73aca82 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/FilteredAnchorTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/FilteredAnchorTask.kt
@@ -62,7 +62,10 @@
* match the requested path prefix and task name.
*/
internal fun Project.addFilterableTasks(vararg taskProviders: TaskProvider<*>?) {
- if (hasProperty(PROP_PATH_PREFIX) && hasProperty(PROP_TASK_NAME)) {
+ if (
+ providers.gradleProperty(PROP_PATH_PREFIX).isPresent &&
+ providers.gradleProperty(PROP_TASK_NAME).isPresent
+ ) {
val pathPrefixes = (properties[PROP_PATH_PREFIX] as String).split(",")
if (pathPrefixes.any { pathPrefix -> relativePathForFiltering().startsWith(pathPrefix) }) {
val taskName = properties[PROP_TASK_NAME] as String
@@ -84,7 +87,10 @@
* -Pandroidx.taskName=checkApi -Pandroidx.pathPrefix=core/core/
*/
internal fun Project.maybeRegisterFilterableTask() {
- if (hasProperty(PROP_TASK_NAME) && hasProperty(PROP_PATH_PREFIX)) {
+ if (
+ providers.gradleProperty(PROP_TASK_NAME).isPresent &&
+ providers.gradleProperty(PROP_PATH_PREFIX).isPresent
+ ) {
tasks.register(GLOBAL_TASK_NAME, FilteredAnchorTask::class.java) { task ->
task.pathPrefix = properties[PROP_PATH_PREFIX] as String
task.taskName = properties[PROP_TASK_NAME] as String
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt b/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt
index c215eea..0b0ffd7 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt
@@ -29,7 +29,10 @@
import java.io.File
import java.io.StringWriter
import org.dom4j.Element
+import org.dom4j.Namespace
+import org.dom4j.QName
import org.dom4j.io.XMLWriter
+import org.dom4j.tree.DefaultText
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.XmlProvider
@@ -273,6 +276,10 @@
}
}
+private val ARTIFACT_ID = QName("artifactId", Namespace("", "http://maven.apache.org/POM/4.0.0"))
+
+private fun Element.textElements() = content().filterIsInstance<DefaultText>()
+
/** Looks for a dependencies XML element within [pom] and sorts its contents. */
fun sortPomDependencies(pom: String): String {
// Workaround for using the default namespace in dom4j.
@@ -284,7 +291,16 @@
element ->
val deps = element.elements()
val sortedDeps = deps.toSortedSet(compareBy { it.stringValue }).toList()
-
+ sortedDeps.map { // b/356612738 https://github.com/gradle/gradle/issues/30112
+ val itsArtifactId = it.element(ARTIFACT_ID)
+ if (itsArtifactId.stringValue.endsWith("-debug")) {
+ itsArtifactId.textElements().last().text =
+ itsArtifactId.textElements().last().text.removeSuffix("-debug")
+ } else if (itsArtifactId.stringValue.endsWith("-release")) {
+ itsArtifactId.textElements().last().text =
+ itsArtifactId.textElements().last().text.removeSuffix("-release")
+ }
+ }
// Content contains formatting nodes, so to avoid modifying those we replace
// each element with the sorted element from its respective index. Note this
// will not move adjacent elements, so any comments would remain in their
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/OwnersService.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/OwnersService.kt
index fa2856a..7b7547a 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/OwnersService.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/OwnersService.kt
@@ -17,6 +17,7 @@
package androidx.build.testConfiguration
import androidx.build.getDistributionDirectory
+import androidx.build.getSupportRootFolder
import com.google.gson.GsonBuilder
import java.io.File
import org.gradle.api.DefaultTask
@@ -76,9 +77,23 @@
task.includeEmptyDirs = false
}
- tasks.register("createModuleInfo", ModuleInfoGenerator::class.java) { task ->
+ tasks.register(CREATE_MODULE_INFO, ModuleInfoGenerator::class.java) { task ->
task.outputFile.set(File(getDistributionDirectory(), "module-info.json"))
}
}
+internal fun Project.addToModuleInfo(testName: String) {
+ rootProject.tasks.named(CREATE_MODULE_INFO).configure {
+ it as ModuleInfoGenerator
+ it.testModules.add(
+ TestModule(
+ name = testName,
+ path = listOf(projectDir.toRelativeString(getSupportRootFolder()))
+ )
+ )
+ }
+}
+
data class TestModule(val name: String, val path: List<String>)
+
+private const val CREATE_MODULE_INFO = "createModuleInfo"
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
index 144e37e..2bd1ab6 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
@@ -23,7 +23,6 @@
import androidx.build.deviceTestsForEachCompat
import androidx.build.getFileInTestConfigDirectory
import androidx.build.getPrivacySandboxFilesDirectory
-import androidx.build.getSupportRootFolder
import androidx.build.hasBenchmarkPlugin
import androidx.build.isMacrobenchmark
import androidx.build.isPresubmitBuild
@@ -192,14 +191,7 @@
rootProject.tasks
.findByName(FINALIZE_TEST_CONFIGS_WITH_APKS_TASK)!!
.dependsOn(generateTestConfigurationTask)
- rootProject.tasks.named<ModuleInfoGenerator>("createModuleInfo").configure {
- it.testModules.add(
- TestModule(
- name = xmlName,
- path = listOf(projectDir.toRelativeString(getSupportRootFolder()))
- )
- )
- }
+ addToModuleInfo(testName = xmlName)
}
/**
@@ -414,41 +406,30 @@
}Tests$variantName.json"
}
- fun ModuleInfoGenerator.addTestModule(clientToT: Boolean, serviceToT: Boolean) {
+ fun Project.addTestModule(clientToT: Boolean, serviceToT: Boolean) {
// We don't test the combination of previous versions of service and client as that is not
// useful data. We always want at least one tip of tree project.
if (!clientToT && !serviceToT) return
- testModules.add(
- TestModule(
- name =
- getJsonName(clientToT = clientToT, serviceToT = serviceToT, clientTests = true),
- path = listOf(projectDir.toRelativeString(getSupportRootFolder()))
- )
+ addToModuleInfo(
+ testName =
+ getJsonName(clientToT = clientToT, serviceToT = serviceToT, clientTests = true)
)
- testModules.add(
- TestModule(
- name =
- getJsonName(
- clientToT = clientToT,
- serviceToT = serviceToT,
- clientTests = false
- ),
- path = listOf(projectDir.toRelativeString(getSupportRootFolder()))
- )
+ addToModuleInfo(
+ testName =
+ getJsonName(clientToT = clientToT, serviceToT = serviceToT, clientTests = false)
)
}
val isClient = this.name.contains("client")
val isPrevious = this.name.contains("previous")
- rootProject.tasks.named<ModuleInfoGenerator>("createModuleInfo").configure {
- if (isClient) {
- it.addTestModule(clientToT = !isPrevious, serviceToT = false)
- it.addTestModule(clientToT = !isPrevious, serviceToT = true)
- } else {
- it.addTestModule(clientToT = true, serviceToT = !isPrevious)
- it.addTestModule(clientToT = false, serviceToT = !isPrevious)
- }
+ if (isClient) {
+ addTestModule(clientToT = !isPrevious, serviceToT = false)
+ addTestModule(clientToT = !isPrevious, serviceToT = true)
+ } else {
+ addTestModule(clientToT = true, serviceToT = !isPrevious)
+ addTestModule(clientToT = false, serviceToT = !isPrevious)
}
+
mediaTask.configure {
if (isClient) {
if (isPrevious) {
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
index b6e55c4..eace746 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
@@ -30,7 +30,6 @@
import androidx.camera.camera2.pipe.integration.impl.StillCaptureRequestControl
import androidx.camera.camera2.pipe.integration.impl.TorchControl
import androidx.camera.camera2.pipe.integration.impl.UseCaseCamera
-import androidx.camera.camera2.pipe.integration.impl.UseCaseThreads
import androidx.camera.camera2.pipe.integration.impl.ZoomControl
import androidx.camera.camera2.pipe.integration.interop.Camera2CameraControl
import androidx.camera.camera2.pipe.integration.interop.CaptureRequestOptions
@@ -49,8 +48,8 @@
import androidx.camera.core.impl.utils.futures.Futures
import com.google.common.util.concurrent.ListenableFuture
import javax.inject.Inject
+import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.async
/**
* Adapt the [CameraControlInternal] interface to [CameraPipe].
@@ -71,7 +70,6 @@
private val focusMeteringControl: FocusMeteringControl,
private val stillCaptureRequestControl: StillCaptureRequestControl,
private val torchControl: TorchControl,
- private val threads: UseCaseThreads,
private val zoomControl: ZoomControl,
private val zslControl: ZslControl,
public val camera2cameraControl: Camera2CameraControl,
@@ -112,11 +110,10 @@
override fun cancelFocusAndMetering(): ListenableFuture<Void> {
return Futures.nonCancellationPropagating(
- threads.sequentialScope
- .async {
- focusMeteringControl.cancelFocusAndMeteringAsync().join()
+ CompletableDeferred<Void?>()
+ .also {
// Convert to null once the task is done, ignore the results.
- return@async null
+ focusMeteringControl.cancelFocusAndMeteringAsync().propagateTo(it) { null }
}
.asListenableFuture()
)
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt
index 852e9b2..e1b06bb6 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt
@@ -48,6 +48,11 @@
camera2InteropCallbacks: CameraInteropStateCallbackRepository,
availableCamerasSelector: CameraSelector?,
) : CameraFactory {
+ private val cameraCoordinator: CameraCoordinatorAdapter =
+ CameraCoordinatorAdapter(
+ lazyCameraPipe.value,
+ lazyCameraPipe.value.cameras(),
+ )
private val appComponent: CameraAppComponent by lazy {
Debug.traceStart { "CameraFactoryAdapter#appComponent" }
val timeSource = SystemTimeSource()
@@ -59,7 +64,8 @@
context,
threadConfig,
lazyCameraPipe.value,
- camera2InteropCallbacks
+ camera2InteropCallbacks,
+ cameraCoordinator
)
)
.build()
@@ -68,11 +74,6 @@
result
}
private val availableCameraIds: LinkedHashSet<String>
- private val cameraCoordinator: CameraCoordinatorAdapter =
- CameraCoordinatorAdapter(
- appComponent.getCameraPipe(),
- appComponent.getCameraDevices(),
- )
init {
val optimizedCameraIds =
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapters.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapters.kt
index 76d7012..5353105 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapters.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapters.kt
@@ -74,21 +74,48 @@
return CallbackToFutureAdapter.getFuture(resolver)
}
+/**
+ * Propagates the result of this to `destination` parameter when this deferred is completed.
+ *
+ * Cancelling the destination is no-op returned from this function does not cancel the `Deferred`
+ * returned by `block`.
+ */
public fun <T> Deferred<T>.propagateTo(destination: CompletableDeferred<T>) {
- invokeOnCompletion { propagateOnceTo(destination, it) }
+ invokeOnCompletion { propagateCompletion(destination, it) }
}
-@OptIn(ExperimentalCoroutinesApi::class)
-public fun <T> Deferred<T>.propagateOnceTo(
- destination: CompletableDeferred<T>,
- throwable: Throwable?,
+/**
+ * Propagates the result of this to `destination` parameter when this deferred is completed.
+ *
+ * Cancelling the destination is no-op returned from this function does not cancel the `Deferred`
+ * returned by `block`.
+ *
+ * @param destination The destination [CompletableDeferred] to which result is propagated to.
+ * @param transform Transformation function to convert the result during propagation.
+ */
+public fun <T, R> Deferred<T>.propagateTo(
+ destination: CompletableDeferred<R>,
+ transform: (T) -> R,
) {
- if (throwable != null) {
- if (throwable is CancellationException) {
- destination.cancel(throwable)
- } else {
- destination.completeExceptionally(throwable)
- }
+ invokeOnCompletion { propagateCompletion(destination, it, transform) }
+}
+
+/**
+ * Propagates the result of this to `destination` parameter immediately.
+ *
+ * This function assumes that [Deferred.invokeOnCompletion] has already been invoked.
+ *
+ * @param destination The destination `Deferred` to which result is propagated to.
+ * @param completionCause The `Throwable` cause of completion that was passed in
+ * `Deferred.invokeOnCompletion`.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+public fun <T> Deferred<T>.propagateCompletion(
+ destination: CompletableDeferred<T>,
+ completionCause: Throwable?,
+) {
+ if (completionCause != null) {
+ destination.completeFailing(completionCause)
} else {
// Ignore exceptions - This should never throw in this situation.
destination.complete(getCompleted())
@@ -96,6 +123,46 @@
}
/**
+ * Propagates the result of this to `destination` parameter immediately.
+ *
+ * This function assumes that [Deferred.invokeOnCompletion] has already been invoked.
+ *
+ * @param destination The destination `Deferred` to which result is propagated to.
+ * @param completionCause The `Throwable` cause of completion that was passed in
+ * `Deferred.invokeOnCompletion`.
+ * @param transform Transformation function to convert the result during propagation.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+public fun <T, R> Deferred<T>.propagateCompletion(
+ destination: CompletableDeferred<R>,
+ completionCause: Throwable?,
+ transform: (T) -> R,
+) {
+ if (completionCause != null) {
+ destination.completeFailing(completionCause)
+ } else {
+ // Ignore exceptions - This should never throw in this situation.
+ destination.complete(transform(getCompleted()))
+ }
+}
+
+/**
+ * Completes this `Deferred` as failure based on the provided `cause`.
+ *
+ * @param cause If it's an instance of [CancellationException], [Deferred.cancel] is invoked for
+ * this, otherwise, [CompletableDeferred.completeExceptionally] is invoked.
+ */
+public fun <T> CompletableDeferred<T>.completeFailing(
+ cause: Throwable,
+) {
+ if (cause is CancellationException) {
+ cancel(cause)
+ } else {
+ completeExceptionally(cause)
+ }
+}
+
+/**
* Waits for [Deferred.await] to be completed until the given timeout.
*
* @return true if `Deferred.await` had completed, false otherwise.
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatBaseImpl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatBaseImpl.kt
index 7aa48d6..dc61707 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatBaseImpl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatBaseImpl.kt
@@ -35,9 +35,13 @@
try {
streamConfigurationMap?.outputFormats
} catch (e: NullPointerException) {
- Logger.e(TAG, "Failed to get output formats from StreamConfigurationMap", e)
+ Logger.w(TAG, "Failed to get output formats from StreamConfigurationMap", e)
+ null
+ } catch (e: IllegalArgumentException) {
+ Logger.w(TAG, "Failed to get output formats from StreamConfigurationMap", e)
null
}
+
return outputFormats?.toTypedArray()
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraAppConfig.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraAppConfig.kt
index c6ebdf0..65f44eb 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraAppConfig.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraAppConfig.kt
@@ -20,6 +20,7 @@
import androidx.camera.camera2.pipe.CameraDevices
import androidx.camera.camera2.pipe.CameraPipe
import androidx.camera.camera2.pipe.integration.impl.CameraInteropStateCallbackRepository
+import androidx.camera.core.concurrent.CameraCoordinator
import androidx.camera.core.impl.CameraFactory
import androidx.camera.core.impl.CameraThreadConfig
import dagger.Component
@@ -44,7 +45,8 @@
private val context: Context,
private val cameraThreadConfig: CameraThreadConfig,
private val cameraPipe: CameraPipe,
- private val camera2InteropCallbacks: CameraInteropStateCallbackRepository
+ private val camera2InteropCallbacks: CameraInteropStateCallbackRepository,
+ private val cameraCoordinator: CameraCoordinator
) {
@Provides public fun provideContext(): Context = context
@@ -55,6 +57,8 @@
@Provides
public fun provideCamera2InteropCallbacks(): CameraInteropStateCallbackRepository =
camera2InteropCallbacks
+
+ @Provides public fun provideCameraCoordinator(): CameraCoordinator = cameraCoordinator
}
/** Dagger component for Application (Process) scoped dependencies. */
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
index 82da040..2080cb8 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
@@ -45,6 +45,7 @@
import androidx.camera.core.impl.UseCaseConfigFactory
import androidx.camera.core.impl.UseCaseConfigFactory.CaptureType
import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.core.internal.TargetConfig.OPTION_TARGET_NAME
import kotlin.math.min
private val DEFAULT_PREVIEW_SIZE = Size(0, 0)
@@ -203,6 +204,7 @@
OPTION_SESSION_CONFIG_UNPACKER,
CameraUseCaseAdapter.DefaultSessionOptionsUnpacker
)
+ insertOption(OPTION_TARGET_NAME, "MeteringRepeating")
insertOption(OPTION_CAPTURE_TYPE, CaptureType.METERING_REPEATING)
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/State3AControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/State3AControl.kt
index da6fcdb..fa9fdb7 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/State3AControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/State3AControl.kt
@@ -90,6 +90,14 @@
public var flashMode: Int by updateOnPropertyChange(DEFAULT_FLASH_MODE)
public var template: Int by updateOnPropertyChange(DEFAULT_REQUEST_TEMPLATE)
public var tryExternalFlashAeMode: Boolean by updateOnPropertyChange(false)
+
+ /**
+ * The [CaptureRequest.CONTROL_AE_MODE] that is set to camera if supported.
+ *
+ * If null, a value based on other settings is calculated and available via
+ * [getFinalPreferredAeMode]. If not supported, [getSupportedAeMode] is used to find the next
+ * best option.
+ */
public var preferredAeMode: Int? by updateOnPropertyChange(null)
public var preferredFocusMode: Int? by updateOnPropertyChange(null)
public var preferredAeFpsRange: Range<Int>? by
@@ -118,6 +126,19 @@
}
}
+ /**
+ * Returns the AE mode that is finally set to camera based on all other settings and camera
+ * capabilities.
+ */
+ public fun getFinalSupportedAeMode(): Int =
+ cameraProperties.metadata.getSupportedAeMode(getFinalPreferredAeMode())
+
+ /**
+ * Returns the AE mode that is finally set to camera based on all other settings.
+ *
+ * Note that this may not be supported via the camera and should be sanitized with
+ * [getSupportedAeMode].
+ */
private fun getFinalPreferredAeMode(): Int {
var preferAeMode =
preferredAeMode
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestControl.kt
index 7ffef5d..94f03ea 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestControl.kt
@@ -19,7 +19,7 @@
import androidx.annotation.GuardedBy
import androidx.camera.camera2.pipe.core.Log.debug
import androidx.camera.camera2.pipe.integration.adapter.asListenableFuture
-import androidx.camera.camera2.pipe.integration.adapter.propagateOnceTo
+import androidx.camera.camera2.pipe.integration.adapter.propagateCompletion
import androidx.camera.camera2.pipe.integration.config.CameraScope
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
@@ -197,7 +197,7 @@
}
}
} else {
- propagateOnceTo(submittedRequest.result, cause)
+ propagateCompletion(submittedRequest.result, cause)
}
}
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/TorchControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/TorchControl.kt
index 7d2deb3..75cdcfe 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/TorchControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/TorchControl.kt
@@ -17,6 +17,9 @@
package androidx.camera.camera2.pipe.integration.impl
import android.hardware.camera2.CaptureRequest
+import androidx.camera.camera2.pipe.AeMode
+import androidx.camera.camera2.pipe.core.Log.debug
+import androidx.camera.camera2.pipe.core.Log.warn
import androidx.camera.camera2.pipe.integration.adapter.propagateTo
import androidx.camera.camera2.pipe.integration.compat.workaround.isFlashAvailable
import androidx.camera.camera2.pipe.integration.config.CameraScope
@@ -86,6 +89,8 @@
cancelPreviousTask: Boolean = true,
ignoreFlashUnitAvailability: Boolean = false
): Deferred<Unit> {
+ debug { "TorchControl#setTorchAsync: torch = $torch" }
+
val signal = CompletableDeferred<Unit>()
if (!ignoreFlashUnitAvailability && !hasFlashUnit) {
@@ -107,16 +112,31 @@
_updateSignal = signal
- // TODO(b/209757083), handle the failed result of the setTorchAsync().
- requestControl.setTorchAsync(torch).join()
-
- // Hold the internal AE mode to ON while the torch is turned ON.
+ // Hold the internal AE mode to ON while the torch is turned ON. If torch is OFF, a
+ // value of null will make the state3AControl calculate the correct AE mode based on
+ // other settings.
state3AControl.preferredAeMode =
if (torch) CaptureRequest.CONTROL_AE_MODE_ON else null
+ val aeMode: AeMode =
+ AeMode.fromIntOrNull(state3AControl.getFinalSupportedAeMode())
+ ?: run {
+ warn {
+ "TorchControl#setTorchAsync: Failed to convert ae mode of value" +
+ " ${state3AControl.getFinalSupportedAeMode()} with" +
+ " AeMode.fromIntOrNull, fallback to AeMode.ON"
+ }
+ AeMode.ON
+ }
- // Always update3A again to reset the AE state in the Camera-pipe controller.
- state3AControl.invalidate()
- state3AControl.updateSignal?.propagateTo(signal) ?: run { signal.complete(Unit) }
+ val deferred =
+ if (torch) requestControl.setTorchOnAsync()
+ else requestControl.setTorchOffAsync(aeMode)
+ deferred.propagateTo(signal) {
+ // TODO: b/209757083 - handle the failed result of the setTorchAsync().
+ // Since we are not handling the result here, signal is completed with Unit
+ // value here without exception when source deferred completes (returning Unit
+ // explicitly is redundant and thus this block looks empty)
+ }
}
}
?: run {
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt
index 0731bda..c105c916 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt
@@ -28,7 +28,6 @@
import androidx.camera.camera2.pipe.RequestTemplate
import androidx.camera.camera2.pipe.Result3A
import androidx.camera.camera2.pipe.StreamId
-import androidx.camera.camera2.pipe.TorchState
import androidx.camera.camera2.pipe.core.Log.debug
import androidx.camera.camera2.pipe.integration.config.UseCaseCameraScope
import androidx.camera.camera2.pipe.integration.config.UseCaseGraphConfig
@@ -121,12 +120,20 @@
// 3A
/**
- * Asynchronously sets the torch (flashlight) state.
+ * Asynchronously sets the torch (flashlight) to ON state.
*
- * @param enabled True to enable the torch, false to disable it.
* @return A [Deferred] representing the asynchronous operation and its result ([Result3A]).
*/
- public suspend fun setTorchAsync(enabled: Boolean): Deferred<Result3A>
+ public suspend fun setTorchOnAsync(): Deferred<Result3A>
+
+ /**
+ * Asynchronously sets the torch (flashlight) state to OFF state.
+ *
+ * @param aeMode The [AeMode] to set while setting the torch value. See
+ * [CameraGraph.Session.setTorchOff] for details.
+ * @return A [Deferred] representing the asynchronous operation and its result ([Result3A]).
+ */
+ public suspend fun setTorchOffAsync(aeMode: AeMode): Deferred<Result3A>
/**
* Asynchronously starts a 3A (Auto Exposure, Auto Focus, Auto White Balance) operation with the
@@ -264,14 +271,14 @@
)
} ?: canceledResult
- override suspend fun setTorchAsync(enabled: Boolean): Deferred<Result3A> =
+ override suspend fun setTorchOnAsync(): Deferred<Result3A> =
+ runIfNotClosed { useGraphSessionOrFailed { it.setTorchOn() } } ?: submitFailedResult
+
+ override suspend fun setTorchOffAsync(aeMode: AeMode): Deferred<Result3A> =
runIfNotClosed {
useGraphSessionOrFailed {
- it.setTorch(
- when (enabled) {
- true -> TorchState.ON
- false -> TorchState.OFF
- }
+ it.setTorchOff(
+ aeMode = aeMode,
)
}
} ?: submitFailedResult
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
index 54adf25..fefc538 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
@@ -17,6 +17,7 @@
package androidx.camera.camera2.pipe.integration.impl
import android.content.Context
+import android.graphics.ImageFormat
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraDevice.TEMPLATE_PREVIEW
import android.hardware.camera2.CaptureRequest
@@ -55,23 +56,31 @@
import androidx.camera.camera2.pipe.integration.config.UseCaseCameraComponent
import androidx.camera.camera2.pipe.integration.config.UseCaseCameraConfig
import androidx.camera.camera2.pipe.integration.config.UseCaseGraphConfig
+import androidx.camera.camera2.pipe.integration.internal.DynamicRangeResolver
import androidx.camera.camera2.pipe.integration.interop.Camera2CameraControl
import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop
import androidx.camera.core.DynamicRange
+import androidx.camera.core.ImageCapture
import androidx.camera.core.MirrorMode
+import androidx.camera.core.Preview
import androidx.camera.core.UseCase
+import androidx.camera.core.concurrent.CameraCoordinator
+import androidx.camera.core.impl.AttachedSurfaceInfo
import androidx.camera.core.impl.CameraControlInternal
import androidx.camera.core.impl.CameraInfoInternal
import androidx.camera.core.impl.CameraInternal
import androidx.camera.core.impl.CameraMode
import androidx.camera.core.impl.CaptureConfig
import androidx.camera.core.impl.DeferrableSurface
-import androidx.camera.core.impl.PreviewConfig
+import androidx.camera.core.impl.MutableOptionsBundle
import androidx.camera.core.impl.SessionConfig
import androidx.camera.core.impl.SessionConfig.OutputConfig.SURFACE_GROUP_ID_NONE
import androidx.camera.core.impl.SessionConfig.ValidatingBuilder
import androidx.camera.core.impl.SessionProcessor
+import androidx.camera.core.impl.SurfaceConfig
import androidx.camera.core.impl.stabilization.StabilizationMode
+import androidx.camera.core.streamsharing.StreamSharing
+import androidx.camera.core.streamsharing.StreamSharingConfig
import javax.inject.Inject
import javax.inject.Provider
import kotlinx.coroutines.Deferred
@@ -111,6 +120,7 @@
@Inject
constructor(
private val cameraPipe: CameraPipe,
+ @GuardedBy("lock") private val cameraCoordinator: CameraCoordinator,
private val callbackMap: CameraCallbackMap,
private val requestListener: ComboRequestListener,
private val cameraConfig: CameraConfig,
@@ -171,6 +181,8 @@
)
}
+ private val dynamicRangeResolver = DynamicRangeResolver(cameraProperties.metadata)
+
@Volatile private var _activeComponent: UseCaseCameraComponent? = null
public val camera: UseCaseCamera?
get() = _activeComponent?.getUseCaseCamera()
@@ -602,7 +614,7 @@
return activeSurfaces > 0 &&
with(attachedUseCases.withoutMetering()) {
(onlyVideoCapture() || requireMeteringRepeating()) &&
- supportMeteringCombination()
+ isMeteringCombinationSupported()
}
}
return false
@@ -624,7 +636,7 @@
return activeSurfaces == 0 ||
with(attachedUseCases.withoutMetering()) {
!(onlyVideoCapture() || requireMeteringRepeating()) ||
- !supportMeteringCombination()
+ !isMeteringCombinationSupported()
}
}
return false
@@ -664,46 +676,140 @@
}
}
- private fun Collection<UseCase>.supportMeteringCombination(): Boolean {
- val useCases = this.toMutableList().apply { add(meteringRepeating) }
+ private fun Collection<UseCase>.isMeteringCombinationSupported(): Boolean {
if (meteringRepeating.attachedSurfaceResolution == null) {
meteringRepeating.setupSession()
}
- return isCombinationSupported(useCases).also {
- Log.debug { "Combination of $useCases is supported: $it" }
+
+ val attachedSurfaceInfoList = getAttachedSurfaceInfoList()
+
+ if (attachedSurfaceInfoList.isEmpty()) {
+ return false
}
+
+ val sessionSurfacesConfigs = getSessionSurfacesConfigs()
+
+ return supportedSurfaceCombination
+ .checkSupported(
+ SupportedSurfaceCombination.FeatureSettings(
+ getCameraMode(),
+ getRequiredMaxBitDepth(attachedSurfaceInfoList),
+ isPreviewStabilizationOn(),
+ isUltraHdrOn()
+ ),
+ mutableListOf<SurfaceConfig>().apply {
+ addAll(sessionSurfacesConfigs)
+ add(createMeteringRepeatingSurfaceConfig())
+ }
+ )
+ .also {
+ Log.debug {
+ "Combination of $sessionSurfacesConfigs + $meteringRepeating is supported: $it"
+ }
+ }
}
- private fun isCombinationSupported(currentUseCases: Collection<UseCase>): Boolean {
- val surfaceConfigs =
- currentUseCases.map { useCase ->
- // TODO: Test with correct Camera Mode when concurrent mode / ultra high resolution
- // is
- // implemented.
- supportedSurfaceCombination.transformSurfaceConfig(
- CameraMode.DEFAULT,
- useCase.imageFormat,
- useCase.attachedSurfaceResolution!!
+ private fun getCameraMode(): Int {
+ synchronized(lock) {
+ if (
+ cameraCoordinator.cameraOperatingMode ==
+ CameraCoordinator.CAMERA_OPERATING_MODE_CONCURRENT
+ ) {
+ return CameraMode.CONCURRENT_CAMERA
+ }
+ }
+
+ return CameraMode.DEFAULT
+ }
+
+ private fun getRequiredMaxBitDepth(attachedSurfaceInfoList: List<AttachedSurfaceInfo>): Int {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ dynamicRangeResolver
+ .resolveAndValidateDynamicRanges(
+ attachedSurfaceInfoList,
+ listOf(meteringRepeating.currentConfig),
+ listOf(0)
+ )
+ .forEach { (_, u) ->
+ if (u.bitDepth == DynamicRange.BIT_DEPTH_10_BIT) {
+ return DynamicRange.BIT_DEPTH_10_BIT
+ }
+ }
+ }
+
+ return DynamicRange.BIT_DEPTH_8_BIT
+ }
+
+ private fun Collection<UseCase>.getAttachedSurfaceInfoList(): List<AttachedSurfaceInfo> =
+ mutableListOf<AttachedSurfaceInfo>().apply {
+ [email protected] { useCase ->
+ val surfaceResolution = useCase.attachedSurfaceResolution
+ val streamSpec = useCase.attachedStreamSpec
+
+ // When collecting the info, the UseCases might be unbound to make these info
+ // become null.
+ if (surfaceResolution == null || streamSpec == null) {
+ Log.warn { "Invalid surface resolution or stream spec is found." }
+ clear()
+ return@apply
+ }
+
+ val surfaceConfig =
+ supportedSurfaceCombination.transformSurfaceConfig(
+ getCameraMode(),
+ useCase.currentConfig.inputFormat,
+ surfaceResolution
+ )
+ add(
+ AttachedSurfaceInfo.create(
+ surfaceConfig,
+ useCase.currentConfig.inputFormat,
+ surfaceResolution,
+ streamSpec.dynamicRange,
+ useCase.getCaptureTypes(),
+ streamSpec.implementationOptions ?: MutableOptionsBundle.create(),
+ useCase.currentConfig.getTargetFrameRate(null)
+ )
)
}
+ }
- var isPreviewStabilizationOn = false
- for (useCase in currentUseCases) {
- if (useCase.currentConfig is PreviewConfig) {
- isPreviewStabilizationOn =
- useCase.currentConfig.previewStabilizationMode == StabilizationMode.ON
+ private fun UseCase.getCaptureTypes() =
+ if (this is StreamSharing) {
+ (currentConfig as StreamSharingConfig).captureTypes
+ } else {
+ listOf(currentConfig.captureType)
+ }
+
+ private fun Collection<UseCase>.isPreviewStabilizationOn() =
+ filterIsInstance<Preview>().firstOrNull()?.currentConfig?.previewStabilizationMode ==
+ StabilizationMode.ON
+
+ private fun Collection<UseCase>.isUltraHdrOn() =
+ filterIsInstance<ImageCapture>().firstOrNull()?.currentConfig?.inputFormat ==
+ ImageFormat.JPEG_R
+
+ private fun Collection<UseCase>.getSessionSurfacesConfigs(): List<SurfaceConfig> =
+ mutableListOf<SurfaceConfig>().apply {
+ [email protected] { useCase ->
+ useCase.sessionConfig.surfaces.forEach { deferrableSurface ->
+ add(
+ supportedSurfaceCombination.transformSurfaceConfig(
+ getCameraMode(),
+ useCase.currentConfig.inputFormat,
+ deferrableSurface.prescribedSize
+ )
+ )
+ }
}
}
- return supportedSurfaceCombination.checkSupported(
- SupportedSurfaceCombination.FeatureSettings(
- CameraMode.DEFAULT,
- DynamicRange.BIT_DEPTH_8_BIT,
- isPreviewStabilizationOn
- ),
- surfaceConfigs
+ private fun createMeteringRepeatingSurfaceConfig() =
+ supportedSurfaceCombination.transformSurfaceConfig(
+ getCameraMode(),
+ meteringRepeating.imageFormat,
+ meteringRepeating.attachedSurfaceResolution!!
)
- }
private fun Collection<UseCase>.surfaceCount(): Int =
ValidatingBuilder().let { validatingBuilder ->
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapterTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapterTest.kt
index 9b27a11..28b47b6 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapterTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapterTest.kt
@@ -46,6 +46,23 @@
}
@Test
+ fun propagateTransformedCompleteResult(): Unit = runBlocking {
+ // Arrange.
+ val resultValue = 123
+ val resultValueTransformed = resultValue.toString()
+
+ val sourceDeferred = CompletableDeferred<Int>()
+ val resultDeferred = CompletableDeferred<String>()
+ sourceDeferred.propagateTo(resultDeferred) { res -> res.toString() }
+
+ // Act.
+ sourceDeferred.complete(resultValue)
+
+ // Assert.
+ assertThat(resultDeferred.await()).isEqualTo(resultValueTransformed)
+ }
+
+ @Test
fun propagateCancelResult() {
// Arrange.
val sourceDeferred = CompletableDeferred<Unit>()
@@ -59,6 +76,20 @@
assertThat(resultDeferred.isCancelled).isTrue()
}
+ @Test
+ fun propagateCancelResult_whenTransformFunctionIsUsed() {
+ // Arrange.
+ val sourceDeferred = CompletableDeferred<Unit>()
+ val resultDeferred = CompletableDeferred<Unit>()
+ sourceDeferred.propagateTo(resultDeferred) { res -> res.toString() }
+
+ // Act.
+ sourceDeferred.cancel()
+
+ // Assert.
+ assertThat(resultDeferred.isCancelled).isTrue()
+ }
+
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun propagateExceptionResult() {
@@ -74,4 +105,20 @@
// Assert.
assertThat(resultDeferred.getCompletionExceptionOrNull()).isSameInstanceAs(testThrowable)
}
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun propagateExceptionResult_whenTransformFunctionIsUsed() {
+ // Arrange.
+ val sourceDeferred = CompletableDeferred<Unit>()
+ val resultDeferred = CompletableDeferred<Unit>()
+ sourceDeferred.propagateTo(resultDeferred) { res -> res.toString() }
+ val testThrowable = Throwable()
+
+ // Act.
+ sourceDeferred.completeExceptionally(testThrowable)
+
+ // Assert.
+ assertThat(resultDeferred.getCompletionExceptionOrNull()).isSameInstanceAs(testThrowable)
+ }
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/RequestProcessorAdapterTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/RequestProcessorAdapterTest.kt
index a933a0a..d9e49d0 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/RequestProcessorAdapterTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/RequestProcessorAdapterTest.kt
@@ -168,6 +168,8 @@
val callback: RequestProcessor.Callback = mock()
requestProcessorAdapter!!.setRepeating(requestToSet, callback)
+ advanceUntilIdle()
+
val frame = cameraGraphSimulator!!.simulateNextFrame()
val request = frame.request
assertThat(request.streams.size).isEqualTo(1)
@@ -209,6 +211,8 @@
val callback: RequestProcessor.Callback = mock()
requestProcessorAdapter!!.submit(mutableListOf(requestToSubmit), callback)
+ advanceUntilIdle()
+
val frame = cameraGraphSimulator!!.simulateNextFrame()
val request = frame.request
assertThat(request.streams.size).isEqualTo(1)
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt
index f968130..5dffedb 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt
@@ -136,8 +136,14 @@
val torchUpdateEventList = mutableListOf<Boolean>()
val setTorchSemaphore = Semaphore(0)
- override suspend fun setTorchAsync(enabled: Boolean): Deferred<Result3A> {
- torchUpdateEventList.add(enabled)
+ override suspend fun setTorchOnAsync(): Deferred<Result3A> {
+ torchUpdateEventList.add(true)
+ setTorchSemaphore.release()
+ return CompletableDeferred(Result3A(Result3A.Status.OK))
+ }
+
+ override suspend fun setTorchOffAsync(aeMode: AeMode): Deferred<Result3A> {
+ torchUpdateEventList.add(false)
setTorchSemaphore.release()
return CompletableDeferred(Result3A(Result3A.Status.OK))
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
index 37d015b..823d915 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
@@ -35,6 +35,7 @@
import androidx.camera.camera2.pipe.CameraStream
import androidx.camera.camera2.pipe.RequestTemplate
import androidx.camera.camera2.pipe.integration.adapter.BlockingTestDeferrableSurface
+import androidx.camera.camera2.pipe.integration.adapter.CameraCoordinatorAdapter
import androidx.camera.camera2.pipe.integration.adapter.CameraStateAdapter
import androidx.camera.camera2.pipe.integration.adapter.CameraUseCaseAdapter
import androidx.camera.camera2.pipe.integration.adapter.FakeTestUseCase
@@ -228,6 +229,67 @@
}
@Test
+ fun meteringRepeatingEnabled_whenPreviewEnabledWithNoSurfaceProvider() = runTest {
+ // Arrange
+ initializeUseCaseThreads(this)
+ val useCaseManager = createUseCaseManager()
+ val preview = createPreview(/* withSurfaceProvider= */ false)
+ val imageCapture = createImageCapture()
+ useCaseManager.attach(listOf(preview, imageCapture))
+
+ // Act
+ useCaseManager.activate(preview)
+ useCaseManager.activate(imageCapture)
+
+ // Assert
+ val enabledUseCaseClasses =
+ useCaseManager.getRunningUseCasesForTest().map { it::class.java }
+ assertThat(enabledUseCaseClasses)
+ .containsExactly(
+ Preview::class.java,
+ ImageCapture::class.java,
+ MeteringRepeating::class.java
+ )
+ }
+
+ @Test
+ fun meteringRepeatingNotEnabled_whenImageAnalysisAndPreviewWithNoSurfaceProvider() = runTest {
+ // Arrange
+ initializeUseCaseThreads(this)
+ val useCaseManager = createUseCaseManager()
+ val preview = createPreview(/* withSurfaceProvider= */ false)
+ val imageAnalysis =
+ ImageAnalysis.Builder().build().apply {
+ setAnalyzer(useCaseThreads.backgroundExecutor) { image -> image.close() }
+ }
+ useCaseManager.attach(listOf(preview, imageAnalysis))
+
+ // Act
+ useCaseManager.activate(preview)
+ useCaseManager.activate(imageAnalysis)
+
+ // Assert
+ val enabledUseCases = useCaseManager.getRunningUseCasesForTest()
+ assertThat(enabledUseCases).containsExactly(preview, imageAnalysis)
+ }
+
+ @Test
+ fun meteringRepeatingNotEnabled_whenOnlyPreviewWithNoSurfaceProvider() = runTest {
+ // Arrange
+ initializeUseCaseThreads(this)
+ val useCaseManager = createUseCaseManager()
+ val preview = createPreview(/* withSurfaceProvider= */ false)
+ useCaseManager.attach(listOf(preview))
+
+ // Act
+ useCaseManager.activate(preview)
+
+ // Assert
+ val enabledUseCases = useCaseManager.getRunningUseCasesForTest()
+ assertThat(enabledUseCases).containsExactly(preview)
+ }
+
+ @Test
fun meteringRepeatingEnabled_whenOnlyImageCaptureEnabled() = runTest {
// Arrange
initializeUseCaseThreads(this)
@@ -615,9 +677,10 @@
val fakeCameraMetadata =
FakeCameraMetadata(cameraId = cameraId, characteristics = characteristicsMap)
val fakeCamera = FakeCamera()
+ val cameraPipe = CameraPipe(CameraPipe.Config(ApplicationProvider.getApplicationContext()))
return UseCaseManager(
- cameraPipe =
- CameraPipe(CameraPipe.Config(ApplicationProvider.getApplicationContext())),
+ cameraPipe = cameraPipe,
+ cameraCoordinator = CameraCoordinatorAdapter(cameraPipe, cameraPipe.cameras()),
cameraConfig = CameraConfig(cameraId),
callbackMap = CameraCallbackMap(),
requestListener = ComboRequestListener(),
@@ -736,16 +799,18 @@
useCaseList.add(it)
}
- private fun createPreview(): Preview =
+ private fun createPreview(withSurfaceProvider: Boolean = true): Preview =
Preview.Builder()
.setCaptureOptionUnpacker(CameraUseCaseAdapter.DefaultCaptureOptionsUnpacker.INSTANCE)
.setSessionOptionUnpacker(CameraUseCaseAdapter.DefaultSessionOptionsUnpacker)
.build()
.apply {
- setSurfaceProvider(
- CameraXExecutors.mainThreadExecutor(),
- SurfaceTextureProvider.createSurfaceTextureProvider()
- )
+ if (withSurfaceProvider) {
+ setSurfaceProvider(
+ CameraXExecutors.mainThreadExecutor(),
+ SurfaceTextureProvider.createSurfaceTextureProvider()
+ )
+ }
}
.also {
it.simulateActivation()
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraGraphSession.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraGraphSession.kt
index f3fe9a8..e7a993a 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraGraphSession.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraGraphSession.kt
@@ -29,7 +29,6 @@
import androidx.camera.camera2.pipe.OutputStatus
import androidx.camera.camera2.pipe.Request
import androidx.camera.camera2.pipe.Result3A
-import androidx.camera.camera2.pipe.TorchState
import androidx.camera.camera2.pipe.integration.testing.FakeCameraGraphSession.RequestStatus.ABORTED
import androidx.camera.camera2.pipe.integration.testing.FakeCameraGraphSession.RequestStatus.FAILED
import androidx.camera.camera2.pipe.integration.testing.FakeCameraGraphSession.RequestStatus.TOTAL_CAPTURE_DONE
@@ -104,7 +103,11 @@
throw NotImplementedError("Not used in testing")
}
- override fun setTorch(torchState: TorchState): Deferred<Result3A> {
+ override fun setTorchOn(): Deferred<Result3A> {
+ throw NotImplementedError("Not used in testing")
+ }
+
+ override fun setTorchOff(aeMode: AeMode?): Deferred<Result3A> {
throw NotImplementedError("Not used in testing")
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt
index 9da279b..0c3df52 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt
@@ -123,7 +123,11 @@
return CompletableDeferred(Unit)
}
- override suspend fun setTorchAsync(enabled: Boolean): Deferred<Result3A> {
+ override suspend fun setTorchOnAsync(): Deferred<Result3A> {
+ return setTorchResult
+ }
+
+ override suspend fun setTorchOffAsync(aeMode: AeMode): Deferred<Result3A> {
return setTorchResult
}
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/CameraGraphSimulator.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/CameraGraphSimulator.kt
index 63a3e39..78d0670 100644
--- a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/CameraGraphSimulator.kt
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/CameraGraphSimulator.kt
@@ -34,10 +34,8 @@
import androidx.camera.camera2.pipe.RequestFailure
import androidx.camera.camera2.pipe.StreamId
import androidx.camera.camera2.pipe.media.ImageSource
-import kotlin.collections.removeFirst as removeFirstKt
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.withTimeout
/**
* This class creates a [CameraPipe] and [CameraGraph] instance using a [FakeCameraBackend].
@@ -166,7 +164,7 @@
}
}
- public suspend fun simulateNextFrame(
+ public fun simulateNextFrame(
advanceClockByNanos: Long = 33_366_666 // (2_000_000_000 / (60 / 1.001))
): FrameSimulator =
generateNextFrame().also {
@@ -174,7 +172,7 @@
it.simulateStarted(clockNanos)
}
- private suspend fun generateNextFrame(): FrameSimulator {
+ private fun generateNextFrame(): FrameSimulator {
val captureSequenceProcessor = cameraController.currentCaptureSequenceProcessor
check(captureSequenceProcessor != null) {
"simulateCameraStarted() must be called before frames can be created!"
@@ -183,16 +181,19 @@
// This checks the pending frame queue and polls for the next request. If no request is
// available it will suspend until the next interaction with the request processor.
if (pendingFrameQueue.isEmpty()) {
- val requestSequence =
- withTimeout(timeMillis = 250) { captureSequenceProcessor.nextRequestSequence() }
+ val captureSequence = captureSequenceProcessor.nextCaptureSequence()
+ checkNotNull(captureSequence) {
+ "Failed to simulate a CaptureSequence from $captureSequenceProcessor! Make sure " +
+ "Requests have been submitted or that the repeating Request has been set."
+ }
// Each sequence is processed as a group, and if a sequence contains multiple requests
// the list of requests is processed in order before polling the next sequence.
- for (request in requestSequence.captureRequestList) {
- pendingFrameQueue.add(FrameSimulator(request, requestSequence))
+ for (request in captureSequence.captureRequestList) {
+ pendingFrameQueue.add(FrameSimulator(request, captureSequence))
}
}
- return pendingFrameQueue.removeFirstKt()
+ return pendingFrameQueue.removeAt(0)
}
/** Utility function to simulate the production of a [FakeImage]s for one or more streams. */
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraIds.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraIds.kt
new file mode 100644
index 0000000..7e718b1
--- /dev/null
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraIds.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.camera2.pipe.testing
+
+import androidx.camera.camera2.pipe.CameraId
+import kotlinx.atomicfu.atomic
+
+/**
+ * Utility class for tracking and creating Fake [CameraId] instances for use in testing.
+ *
+ * These id's are intentionally non-numerical to help prevent code that may assume that camera2
+ * camera ids are parsable.
+ */
+public object FakeCameraIds {
+ private val fakeCameraIds = atomic(0)
+ public val default: CameraId = CameraId("FakeCamera-default")
+
+ public fun next(): CameraId = CameraId("FakeCamera-${fakeCameraIds.getAndIncrement()}")
+}
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraMetadata.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraMetadata.kt
index 1280187..38f6520 100644
--- a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraMetadata.kt
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraMetadata.kt
@@ -27,12 +27,6 @@
import androidx.camera.camera2.pipe.CameraMetadata
import androidx.camera.camera2.pipe.Metadata
import kotlin.reflect.KClass
-import kotlinx.atomicfu.atomic
-
-private val fakeCameraIds = atomic(0)
-
-internal fun nextFakeCameraId(): CameraId =
- CameraId("FakeCamera-${fakeCameraIds.incrementAndGet()}")
/** Utility class for interacting with objects that require pre-populated Metadata. */
public open class FakeMetadata(private val metadata: Map<Metadata.Key<*>, Any?> = emptyMap()) :
@@ -56,7 +50,7 @@
public class FakeCameraMetadata(
private val characteristics: Map<CameraCharacteristics.Key<*>, Any?> = emptyMap(),
metadata: Map<Metadata.Key<*>, Any?> = emptyMap(),
- cameraId: CameraId = nextFakeCameraId(),
+ cameraId: CameraId = FakeCameraIds.default,
override val keys: Set<CameraCharacteristics.Key<*>> = emptySet(),
override val requestKeys: Set<CaptureRequest.Key<*>> = emptySet(),
override val resultKeys: Set<CaptureResult.Key<*>> = emptySet(),
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCaptureSequenceProcessor.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCaptureSequenceProcessor.kt
index 5010c90..161d428 100644
--- a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCaptureSequenceProcessor.kt
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCaptureSequenceProcessor.kt
@@ -25,48 +25,81 @@
import androidx.camera.camera2.pipe.RequestTemplate
import androidx.camera.camera2.pipe.StreamId
import kotlinx.atomicfu.atomic
-import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.Deferred
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.withTimeout
/**
- * Fake implementation of a [CaptureSequenceProcessor] that passes events to a [Channel].
+ * Fake implementation of a [CaptureSequenceProcessor] that records events and simulates some low
+ * level behavior.
*
* This allows kotlin tests to check sequences of interactions that dispatch in the background
* without blocking between events.
*/
public class FakeCaptureSequenceProcessor(
- private val cameraId: CameraId = CameraId("test-camera"),
+ private val cameraId: CameraId = FakeCameraIds.default,
private val defaultTemplate: RequestTemplate = RequestTemplate(1)
) : CaptureSequenceProcessor<Request, FakeCaptureSequence> {
+ private val debugId = debugIds.incrementAndGet()
private val lock = Any()
private val sequenceIds = atomic(0)
- private val eventChannel = Channel<Event>(Channel.UNLIMITED)
- @GuardedBy("lock") private var pendingSequence: CompletableDeferred<FakeCaptureSequence>? = null
+ @GuardedBy("lock") private val captureQueue = mutableListOf<FakeCaptureSequence>()
- @GuardedBy("lock") private val queue: MutableList<FakeCaptureSequence> = mutableListOf()
+ @GuardedBy("lock") private var repeatingCapture: FakeCaptureSequence? = null
- @GuardedBy("lock") private var repeatingRequestSequence: FakeCaptureSequence? = null
+ @GuardedBy("lock") private var shutdown = false
- @GuardedBy("lock") private var _rejectRequests = false
+ @GuardedBy("lock") private val _events = mutableListOf<Event>()
- public var rejectRequests: Boolean
- get() = synchronized(lock) { _rejectRequests }
- set(value) {
- synchronized(lock) { _rejectRequests = value }
+ @GuardedBy("lock") private var nextEventIndex = 0
+ public val events: List<Event>
+ get() = synchronized(lock) { _events }
+
+ /** Get the next event from queue with an option to specify a timeout for tests. */
+ public fun nextEvent(): Event {
+ synchronized(lock) {
+ val eventIdx = nextEventIndex++
+ check(_events.size > 0) {
+ "Failed to get next event for $this, there have been no interactions."
+ }
+ check(eventIdx < _events.size) {
+ "Failed to get next event. Last event was ${events[eventIdx - 1]}"
+ }
+ return events[eventIdx]
}
+ }
- private var _surfaceMap: Map<StreamId, Surface> = emptyMap()
- public var surfaceMap: Map<StreamId, Surface>
- get() = synchronized(lock) { _surfaceMap }
+ public fun clearEvents() {
+ synchronized(lock) {
+ _events.clear()
+ nextEventIndex = 0
+ }
+ }
+
+ public var rejectBuild: Boolean = false
+ get() = synchronized(lock) { field }
+ set(value) = synchronized(lock) { field = value }
+
+ public var rejectSubmit: Boolean = false
+ get() = synchronized(lock) { field }
+ set(value) = synchronized(lock) { field = value }
+
+ public var surfaceMap: Map<StreamId, Surface> = emptyMap()
+ get() = synchronized(lock) { field }
set(value) =
synchronized(lock) {
- _surfaceMap = value
+ field = value
println("Configured surfaceMap for $this")
}
+ @Volatile public var throwOnBuild: Boolean = false
+
+ @Volatile public var throwOnSubmit: Boolean = false
+
+ @Volatile public var throwOnStop: Boolean = false
+
+ @Volatile public var throwOnAbort: Boolean = false
+
+ @Volatile public var throwOnShutdown: Boolean = false
+
override fun build(
isRepeating: Boolean,
requests: List<Request>,
@@ -75,136 +108,157 @@
listeners: List<Request.Listener>,
sequenceListener: CaptureSequenceListener
): FakeCaptureSequence? {
- return FakeCaptureSequence.create(
- cameraId = cameraId,
- repeating = isRepeating,
- requests = requests,
- surfaceMap = surfaceMap,
- defaultTemplate = defaultTemplate,
- defaultParameters = defaultParameters,
- requiredParameters = requiredParameters,
- listeners = listeners,
- sequenceListener = sequenceListener
- )
+ throwTestExceptionIf(throwOnBuild)
+
+ val captureSequence =
+ FakeCaptureSequence.create(
+ cameraId,
+ isRepeating,
+ requests,
+ surfaceMap,
+ defaultTemplate,
+ defaultParameters,
+ requiredParameters,
+ listeners,
+ sequenceListener
+ )
+ synchronized(lock) {
+ if (rejectBuild || shutdown || captureSequence == null) {
+ println("$this: BuildRejected $captureSequence")
+ _events.add(BuildRejected(captureSequence))
+ return null
+ }
+ }
+ println("$this: Build $captureSequence")
+ return captureSequence
}
override fun submit(captureSequence: FakeCaptureSequence): Int {
- println("submit $captureSequence")
+ throwTestExceptionIf(throwOnSubmit)
synchronized(lock) {
- if (rejectRequests) {
- check(
- eventChannel
- .trySend(Event(requestSequence = captureSequence, rejected = true))
- .isSuccess
- )
+ if (rejectSubmit || shutdown) {
+ println("$this: SubmitRejected $captureSequence")
+ _events.add(SubmitRejected(captureSequence))
return -1
}
- queue.add(captureSequence)
+ captureQueue.add(captureSequence)
if (captureSequence.repeating) {
- repeatingRequestSequence = captureSequence
+ repeatingCapture = captureSequence
}
- check(
- eventChannel
- .trySend(Event(requestSequence = captureSequence, submit = true))
- .isSuccess
- )
- // If there is a non-null pending sequence, make sure we complete it here.
- pendingSequence?.also {
- pendingSequence = null
- it.complete(captureSequence)
- }
+ println("$this: Submit $captureSequence")
+ _events.add(Submit(captureSequence))
return sequenceIds.incrementAndGet()
}
}
override fun abortCaptures() {
+ throwTestExceptionIf(throwOnAbort)
+
val requestSequencesToAbort: List<FakeCaptureSequence>
synchronized(lock) {
- requestSequencesToAbort = queue.toList()
- queue.clear()
- check(eventChannel.trySend(Event(abort = true)).isSuccess)
+ println("$this: AbortCaptures")
+ _events.add(AbortCaptures)
+ requestSequencesToAbort = captureQueue.toList()
+ captureQueue.clear()
}
+
for (sequence in requestSequencesToAbort) {
sequence.invokeOnSequenceAborted()
}
}
override fun stopRepeating() {
- val requestSequence =
- synchronized(lock) {
- check(eventChannel.trySend(Event(stop = true)).isSuccess)
- repeatingRequestSequence.also { repeatingRequestSequence = null }
- }
- requestSequence?.invokeOnSequenceAborted()
+ throwTestExceptionIf(throwOnStop)
+ synchronized(lock) {
+ println("$this: StopRepeating")
+ _events.add(StopRepeating)
+ repeatingCapture = null
+ }
}
override suspend fun shutdown() {
+ throwTestExceptionIf(throwOnShutdown)
synchronized(lock) {
- rejectRequests = true
- check(eventChannel.trySend(Event(close = true)).isSuccess)
+ println("$this: Shutdown")
+ shutdown = true
+ _events.add(Shutdown)
}
}
- /** Get the next event from queue with an option to specify a timeout for tests. */
- public suspend fun nextEvent(timeMillis: Long = 500): Event =
- withTimeout(timeMillis) { eventChannel.receive() }
+ override fun toString(): String {
+ return "FakeCaptureSequenceProcessor-$debugId($cameraId)"
+ }
- public suspend fun nextRequestSequence(): FakeCaptureSequence {
- while (true) {
- val pending: Deferred<FakeCaptureSequence>
- synchronized(lock) {
- var sequence = queue.removeFirstOrNull()
- if (sequence == null) {
- sequence = repeatingRequestSequence
- }
- if (sequence != null) {
- return sequence
- }
+ /**
+ * Get the next CaptureSequence from this CaptureSequenceProcessor. If there are non-repeating
+ * capture requests in the queue, remove the first item from the queue. Otherwise, return the
+ * current repeating CaptureSequence, or null if there are no active CaptureSequences.
+ */
+ internal fun nextCaptureSequence(): FakeCaptureSequence? =
+ synchronized(lock) { captureQueue.removeFirstOrNull() ?: repeatingCapture }
- if (pendingSequence == null) {
- pendingSequence = CompletableDeferred()
- }
- pending = pendingSequence!!
- }
-
- pending.await()
+ private fun throwTestExceptionIf(condition: Boolean) {
+ if (condition) {
+ throw RuntimeException("Test Exception")
}
}
- /** TODO: It's probably better to model this as a sealed class. */
- public data class Event(
- val requestSequence: FakeCaptureSequence? = null,
- val rejected: Boolean = false,
- val abort: Boolean = false,
- val close: Boolean = false,
- val stop: Boolean = false,
- val submit: Boolean = false
- )
+ public open class Event
+
+ public object Shutdown : Event()
+
+ public object StopRepeating : Event()
+
+ public object AbortCaptures : Event()
+
+ public data class BuildRejected(val captureSequence: FakeCaptureSequence?) : Event()
+
+ public data class SubmitRejected(val captureSequence: FakeCaptureSequence) : Event()
+
+ public data class Submit(val captureSequence: FakeCaptureSequence) : Event()
public companion object {
- public suspend fun FakeCaptureSequenceProcessor.awaitEvent(
- request: Request? = null,
- filter: (event: Event) -> Boolean
- ): Event {
+ private val debugIds = atomic(0)
+ public val Event.requests: List<Request>
+ get() = checkNotNull(captureSequence).captureRequestList
- var event: Event
- var loopCount = 0
- while (loopCount < 10) {
- loopCount++
- event = this.nextEvent()
+ public val Event.requiredParameters: Map<*, Any?>
+ get() = checkNotNull(captureSequence).requiredParameters
- if (request != null) {
- val contains =
- event.requestSequence?.captureRequestList?.contains(request) ?: false
- if (filter(event) && contains) {
- return event
- }
- } else if (filter(event)) {
- return event
+ public val Event.defaultParameters: Map<*, Any?>
+ get() = checkNotNull(captureSequence).defaultParameters
+
+ // TODO: Decide if these should only work on successful submit or not.
+ public val Event.isRepeating: Boolean
+ get() = (this as? Submit)?.captureSequence?.repeating ?: false
+
+ public val Event.isCapture: Boolean
+ get() = (this as? Submit)?.captureSequence?.repeating == false
+
+ public val Event.isRejected: Boolean
+ get() =
+ when (this) {
+ is BuildRejected,
+ is SubmitRejected -> true
+ else -> false
}
- }
- throw IllegalStateException("Failed to observe a submit event containing $request")
- }
+ public val Event.isAbort: Boolean
+ get() = this is AbortCaptures
+
+ public val Event.isStopRepeating: Boolean
+ get() = this is StopRepeating
+
+ public val Event.isClose: Boolean
+ get() = this is Shutdown
+
+ public val Event.captureSequence: FakeCaptureSequence?
+ get() =
+ when (this) {
+ is Submit -> captureSequence
+ is BuildRejected -> captureSequence
+ is SubmitRejected -> captureSequence
+ else -> null
+ }
}
}
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeFrameMetadata.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeFrameMetadata.kt
index 90465f2..f24a252 100644
--- a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeFrameMetadata.kt
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeFrameMetadata.kt
@@ -35,7 +35,7 @@
public class FakeFrameMetadata(
private val resultMetadata: Map<CaptureResult.Key<*>, Any?> = emptyMap(),
extraResultMetadata: Map<Metadata.Key<*>, Any?> = emptyMap(),
- override val camera: CameraId = nextFakeCameraId(),
+ override val camera: CameraId = FakeCameraIds.default,
override val frameNumber: FrameNumber = nextFakeFrameNumber(),
override val extraMetadata: Map<*, Any?> = emptyMap<Any, Any>()
) : FakeMetadata(extraResultMetadata), FrameMetadata {
diff --git a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/CameraPipeSimulatorTest.kt b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/CameraPipeSimulatorTest.kt
index d7373f1..f1e3fd5 100644
--- a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/CameraPipeSimulatorTest.kt
+++ b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/CameraPipeSimulatorTest.kt
@@ -36,13 +36,17 @@
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
class CameraPipeSimulatorTest {
private val testScope = TestScope()
- private val frontCameraMetadata =
- FakeCameraMetadata(
- mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_FRONT)
- )
private val backCameraMetadata =
FakeCameraMetadata(
- mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_BACK)
+ cameraId = FakeCameraIds.next(),
+ characteristics =
+ mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_BACK)
+ )
+ private val frontCameraMetadata =
+ FakeCameraMetadata(
+ cameraId = FakeCameraIds.next(),
+ characteristics =
+ mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_FRONT)
)
private val streamConfig = CameraStream.Config.create(Size(640, 480), StreamFormat.YUV_420_888)
diff --git a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDevicesTest.kt b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDevicesTest.kt
index d91dd8e..fed7ff7 100644
--- a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDevicesTest.kt
+++ b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDevicesTest.kt
@@ -31,22 +31,30 @@
class FakeCameraDevicesTest {
private val EXTERNAL_BACKEND_ID =
CameraBackendId("androidx.camera.camera2.pipe.testing.FakeCameraDevicesTest")
- private val metadata1 =
+ private val frontMetadata =
FakeCameraMetadata(
- mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_FRONT)
+ cameraId = FakeCameraIds.next(),
+ characteristics =
+ mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_FRONT)
)
- private val metadata2 =
+ private val backMetadata =
FakeCameraMetadata(
- mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_BACK)
+ cameraId = FakeCameraIds.next(),
+ characteristics =
+ mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_BACK)
)
- private val metadata3 =
+ private val extMetadata =
FakeCameraMetadata(
- mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_EXTERNAL)
+ cameraId = FakeCameraIds.next(),
+ characteristics =
+ mapOf(
+ CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_EXTERNAL
+ )
)
private val cameraMetadataMap =
mapOf(
- FAKE_CAMERA_BACKEND_ID to listOf(metadata1, metadata2),
- EXTERNAL_BACKEND_ID to listOf(metadata3)
+ FAKE_CAMERA_BACKEND_ID to listOf(frontMetadata, backMetadata),
+ EXTERNAL_BACKEND_ID to listOf(extMetadata)
)
@Test
@@ -63,11 +71,13 @@
)
val devices = cameraDevices.getCameraIds()
assertThat(devices)
- .containsExactlyElementsIn(listOf(metadata1.camera, metadata2.camera))
+ .containsExactlyElementsIn(listOf(frontMetadata.camera, backMetadata.camera))
.inOrder()
- assertThat(cameraDevices.getCameraMetadata(metadata1.camera)).isSameInstanceAs(metadata1)
- assertThat(cameraDevices.getCameraMetadata(metadata2.camera)).isSameInstanceAs(metadata2)
+ assertThat(cameraDevices.getCameraMetadata(frontMetadata.camera))
+ .isSameInstanceAs(frontMetadata)
+ assertThat(cameraDevices.getCameraMetadata(backMetadata.camera))
+ .isSameInstanceAs(backMetadata)
}
@Test
@@ -86,13 +96,13 @@
assertThat(devices)
.containsExactlyElementsIn(
listOf(
- metadata3.camera,
+ extMetadata.camera,
)
)
.inOrder()
- assertThat(cameraDevices.getCameraMetadata(metadata3.camera)).isNull()
- assertThat(cameraDevices.getCameraMetadata(metadata3.camera, EXTERNAL_BACKEND_ID))
- .isSameInstanceAs(metadata3)
+ assertThat(cameraDevices.getCameraMetadata(extMetadata.camera)).isNull()
+ assertThat(cameraDevices.getCameraMetadata(extMetadata.camera, EXTERNAL_BACKEND_ID))
+ .isSameInstanceAs(extMetadata)
}
}
diff --git a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeMetadataTest.kt b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeMetadataTest.kt
index e276d6c..d418346 100644
--- a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeMetadataTest.kt
+++ b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeMetadataTest.kt
@@ -62,7 +62,6 @@
)
assertThat(metadata1).isNotEqualTo(metadata2)
- assertThat(metadata1.camera).isNotEqualTo(metadata2.camera)
}
@Test
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraBackend.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraBackend.kt
index 9ed683c..5d5ee33 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraBackend.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraBackend.kt
@@ -40,7 +40,7 @@
}
public class CameraAvailable(public val cameraId: CameraId) : CameraStatus() {
- override fun toString(): String = "CameraAvailable(camera=$cameraId"
+ override fun toString(): String = "CameraAvailable(camera=$cameraId)"
}
}
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraControls.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraControls.kt
index 2d032b7..869f124 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraControls.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraControls.kt
@@ -77,6 +77,18 @@
@JvmStatic
public fun fromIntOrNull(value: Int): AeMode? = values.firstOrNull { it.value == value }
+
+ @JvmStatic
+ public fun fromInt(value: Int): AeMode =
+ when (value) {
+ OFF.value -> OFF
+ ON.value -> ON
+ ON_AUTO_FLASH.value -> ON_AUTO_FLASH
+ ON_ALWAYS_FLASH.value -> ON_ALWAYS_FLASH
+ ON_AUTO_FLASH_REDEYE.value -> ON_AUTO_FLASH_REDEYE
+ ON_EXTERNAL_FLASH.value -> ON_EXTERNAL_FLASH
+ else -> ON
+ }
}
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
index e27775d..4e8a4ee 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
@@ -503,20 +503,30 @@
): Deferred<Result3A>
/**
- * Turns the torch to ON or OFF.
+ * Turns the torch to ON.
*
* This method has a side effect on the currently set AE mode. Ref:
* https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#FLASH_MODE
* To use the flash control, AE mode must be set to ON or OFF. So if the AE mode is already
* not either ON or OFF, we will need to update the AE mode to one of those states, here we
* will choose ON. It is the responsibility of the application layer above CameraPipe to
- * restore the AE mode after the torch control has been used. The [update3A] method can be
- * used to restore the AE state to a previous value.
+ * restore the AE mode after the torch control has been used. The [setTorchOff] or
+ * [update3A] method can be used to restore the AE state to a previous value.
*
* @return the FrameNumber at which the turn was fully turned on if switch was ON, or the
* FrameNumber at which it was completely turned off when the switch was OFF.
*/
- public fun setTorch(torchState: TorchState): Deferred<Result3A>
+ public fun setTorchOn(): Deferred<Result3A>
+
+ /**
+ * Turns the torch to OFF.
+ *
+ * @param aeMode The [AeMode] to set while disabling the torch value. If null which is the
+ * default value, the current AE mode is used.
+ * @return the FrameNumber at which the turn was fully turned on if switch was ON, or the
+ * FrameNumber at which it was completely turned off when the switch was OFF.
+ */
+ public fun setTorchOff(aeMode: AeMode? = null): Deferred<Result3A>
/**
* Locks the auto-exposure, auto-focus and auto-whitebalance as per the given desired
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Requests.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Requests.kt
index d75b9f9..89467a3 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Requests.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Requests.kt
@@ -18,6 +18,8 @@
import android.hardware.camera2.CameraCaptureSession
import android.hardware.camera2.CameraDevice
+import android.hardware.camera2.CameraExtensionCharacteristics
+import android.hardware.camera2.CameraExtensionSession
import android.hardware.camera2.CaptureFailure
import android.hardware.camera2.CaptureRequest
import android.view.Surface
@@ -107,6 +109,25 @@
) {}
/**
+ * This event provides clients with an estimate of the post-processing progress of a capture
+ * which could take significantly more time relative to the rest of the
+ * [CameraExtensionSession.capture] sequence. The callback will be triggered only by
+ * extensions that return true from calls
+ * [CameraExtensionCharacteristics.isCaptureProcessProgressAvailable]. If support for this
+ * callback is present, then clients will be notified at least once with progress value 100.
+ * The callback will be triggered only for still capture requests
+ * [CameraExtensionSession.capture] and is not supported for repeating requests
+ * [CameraExtensionSession.setRepeatingRequest].
+ *
+ * @param requestMetadata the data about the camera2 request that was sent to the camera.
+ * @param progress the value indicating the current post-processing progress (between 0 and
+ * 100 inclusive)
+ * @see
+ * android.hardware.camera2.CameraExtensionSession.ExtensionCaptureCallback.onCaptureProcessProgressed
+ */
+ public fun onCaptureProgress(requestMetadata: RequestMetadata, progress: Int) {}
+
+ /**
* This event indicates that all of the metadata associated with this frame has been
* produced. If [onPartialCaptureResult] was invoked, the values returned in the
* totalCaptureResult map be a superset of the values produced from the
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt
index 16a26fd..efe9f68 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt
@@ -176,7 +176,7 @@
ControllerState.ERROR ->
if (
cameraStatus is CameraStatus.CameraAvailable &&
- lastCameraError == CameraError.ERROR_CAMERA_DEVICE
+ lastCameraError != CameraError.ERROR_GRAPH_CONFIG
) {
shouldRestart = true
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureCallback.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureCallback.kt
index 9d10ac0..ebc244d 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureCallback.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureCallback.kt
@@ -46,6 +46,8 @@
frameNumber: FrameNumber
)
+ fun onCaptureProcessProgressed(captureRequest: CaptureRequest, progress: Int)
+
fun onCaptureFailed(captureRequest: CaptureRequest, frameNumber: FrameNumber)
fun onCaptureSequenceCompleted(captureSequenceId: Int, captureFrameNumber: Long)
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequence.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequence.kt
index 34a0a1d..cc5e292 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequence.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequence.kt
@@ -184,6 +184,15 @@
Debug.traceStop() // onCaptureCompleted
}
+ override fun onCaptureProcessProgressed(captureRequest: CaptureRequest, progress: Int) {
+ Debug.traceStart { "onCaptureProcessProgressed" }
+ // Load the request and throw if we are not able to find an associated request. Under
+ // normal circumstances this should never happen.
+ val request = readRequestMetadata(captureRequest)
+ invokeOnRequest(request) { it.onCaptureProgress(request, progress) }
+ Debug.traceStop()
+ }
+
override fun onCaptureFailed(
captureSession: CameraCaptureSession,
captureRequest: CaptureRequest,
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Exceptions.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Exceptions.kt
index 8039abf..f7532e5 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Exceptions.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Exceptions.kt
@@ -55,9 +55,19 @@
} catch (e: Exception) {
Log.warn { "Unexpected error: " + e.message }
when (e) {
+ is CameraAccessException -> {
+ cameraErrorListener.onCameraError(
+ cameraId,
+ CameraError.from(e),
+ // CameraAccessException indicates the task failed because the camera is
+ // unavailable, such as when the camera is in use or disconnected. Such errors
+ // can be recovered when the camera becomes available.
+ willAttemptRetry = true,
+ )
+ return null
+ }
is IllegalArgumentException,
is IllegalStateException,
- is CameraAccessException,
is SecurityException,
is UnsupportedOperationException,
is NullPointerException -> {
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExtensionSessionWrapper.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExtensionSessionWrapper.kt
index 7fb4d94..608dc0f 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExtensionSessionWrapper.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExtensionSessionWrapper.kt
@@ -278,6 +278,14 @@
request: CaptureRequest
) {}
+ override fun onCaptureProcessProgressed(
+ session: CameraExtensionSession,
+ request: CaptureRequest,
+ progress: Int
+ ) {
+ captureCallback.onCaptureProcessProgressed(request, progress)
+ }
+
override fun onCaptureFailed(session: CameraExtensionSession, request: CaptureRequest) {
val frameNumber = frameQueue.remove()
captureCallback.onCaptureFailed(request, FrameNumber(frameNumber))
@@ -342,6 +350,14 @@
}
}
+ override fun onCaptureProcessProgressed(
+ session: CameraExtensionSession,
+ request: CaptureRequest,
+ progress: Int
+ ) {
+ captureCallback.onCaptureProcessProgressed(request, progress)
+ }
+
override fun onCaptureSequenceCompleted(session: CameraExtensionSession, sequenceId: Int) {
val frameNumber = extensionSessionMap[session]
captureCallback.onCaptureSequenceCompleted(sequenceId, frameNumber!!)
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImpl.kt
index c552afc..e89dcf6 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImpl.kt
@@ -26,7 +26,6 @@
import androidx.camera.camera2.pipe.Lock3ABehavior
import androidx.camera.camera2.pipe.Request
import androidx.camera.camera2.pipe.Result3A
-import androidx.camera.camera2.pipe.TorchState
import androidx.camera.camera2.pipe.core.Token
import androidx.camera.camera2.pipe.internal.FrameCaptureQueue
import kotlinx.atomicfu.atomic
@@ -115,11 +114,16 @@
return controller3A.submit3A(aeMode, afMode, awbMode, aeRegions, afRegions, awbRegions)
}
- override fun setTorch(torchState: TorchState): Deferred<Result3A> {
- check(!token.released) { "Cannot call setTorch on $this after close." }
+ override fun setTorchOn(): Deferred<Result3A> {
+ check(!token.released) { "Cannot call setTorchOn on $this after close." }
// TODO(sushilnath): First check whether the camera device has a flash unit. Ref:
// https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#FLASH_INFO_AVAILABLE
- return controller3A.setTorch(torchState)
+ return controller3A.setTorchOn()
+ }
+
+ override fun setTorchOff(aeMode: AeMode?): Deferred<Result3A> {
+ check(!token.released) { "Cannot call setTorchOff on $this after close." }
+ return controller3A.setTorchOff(aeMode)
}
override suspend fun lock3A(
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt
index 177ce94..8c3c597 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt
@@ -43,7 +43,6 @@
import androidx.camera.camera2.pipe.Lock3ABehavior
import androidx.camera.camera2.pipe.Result3A
import androidx.camera.camera2.pipe.Result3A.Status
-import androidx.camera.camera2.pipe.TorchState
import androidx.camera.camera2.pipe.core.Log.debug
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
@@ -646,14 +645,23 @@
return listener.result
}
- fun setTorch(torchState: TorchState): Deferred<Result3A> {
- // Determine the flash mode based on the torch state.
- val flashMode = if (torchState == TorchState.ON) FlashMode.TORCH else FlashMode.OFF
- // To use the flash control, AE mode must be set to ON or OFF.
+ /**
+ * Enables the torch which may require changing the [AeMode].
+ *
+ * To use [FlashMode.TORCH], either [AeMode.ON] or [AeMode.OFF] needs to be used, otherwise, the
+ * flash mode is a no-op. If the current AE mode is neither of them, this function changes the
+ * AE mode to [AeMode.ON] in order to enable the torch.
+ */
+ fun setTorchOn(): Deferred<Result3A> {
val currAeMode = graphState3A.aeMode
val desiredAeMode =
if (currAeMode == AeMode.ON || currAeMode == AeMode.OFF) null else AeMode.ON
- return update3A(aeMode = desiredAeMode, flashMode = flashMode)
+ return update3A(aeMode = desiredAeMode, flashMode = FlashMode.TORCH)
+ }
+
+ /** Disables the torch and sets a new AE mode if provided. */
+ fun setTorchOff(aeMode: AeMode? = null): Deferred<Result3A> {
+ return update3A(aeMode = aeMode, flashMode = FlashMode.OFF)
}
private fun lock3ANow(
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphLoop.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphLoop.kt
index 80ce6b0f..cc5d57a 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphLoop.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphLoop.kt
@@ -373,8 +373,8 @@
if (success) {
lastRepeatingRequest = command.request
commands.removeAt(idx)
+ commands.removeUpTo(idx) { it is StartRepeating }
}
- commands.removeUpTo(idx) { it is StartRepeating }
}
is SubmitCapture -> {
if (!_captureProcessingEnabled.value) {
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt
index 4b35f50..a9e597f 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt
@@ -26,6 +26,7 @@
import androidx.camera.camera2.pipe.RequestNumber
import androidx.camera.camera2.pipe.Result3A
import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.requiredParameters
import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
import androidx.camera.camera2.pipe.testing.FakeGraphProcessor
import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
@@ -477,14 +478,20 @@
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
// We now check if the correct sequence of requests were submitted by unlock3APostCapture
- // call. There should be a request to cancel AF and AE precapture metering.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
+ // call. There should be a request to cancel AF and AE precapture metering
+ val event1 = captureSequenceProcessor.nextEvent()
if (cancelAf) {
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
+ assertThat(event1.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+ )
}
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL)
+ assertThat(event1.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL
+ )
}
private fun testUnlock3APostCaptureAndroidLAndBelow(cancelAf: Boolean = true) = runTest {
@@ -509,31 +516,37 @@
// We now check if the correct sequence of requests were submitted by unlock3APostCapture
// call. There should be a request to cancel AF and lock ae.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
+ val event1 = captureSequenceProcessor.nextEvent()
if (cancelAf) {
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
+ assertThat(event1.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+ )
}
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+
+ assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
// Then another request to unlock ae.
- val request2 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(false)
+ val captureSequence2 = captureSequenceProcessor.nextEvent()
+ assertThat(captureSequence2.requiredParameters)
+ .containsEntry(CaptureRequest.CONTROL_AE_LOCK, false)
}
- private suspend fun assertCorrectCaptureSequenceInLock3AForCapture(
- isAfTriggered: Boolean = true
- ) {
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).apply {
+ private fun assertCorrectCaptureSequenceInLock3AForCapture(isAfTriggered: Boolean = true) {
+ val event1 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).apply {
if (isAfTriggered) {
isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
} else {
isNotEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_IDLE)
}
}
- assertThat(request1.requiredParameters[CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START)
+ assertThat(event1.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START
+ )
}
companion object {
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ALock3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ALock3ATest.kt
index 3085cc7..e9be1c9 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ALock3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ALock3ATest.kt
@@ -27,6 +27,10 @@
import androidx.camera.camera2.pipe.RequestNumber
import androidx.camera.camera2.pipe.Result3A
import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isCapture
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isRepeating
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.requests
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.requiredParameters
import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
import androidx.camera.camera2.pipe.testing.FakeGraphProcessor
import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
@@ -147,14 +151,17 @@
// We not check if the correct sequence of requests were submitted by lock3A call. The
// request should be a repeating request to lock AE.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event1 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
// The second request should be a single request to lock AF.
- val request2 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
- assertThat(request2.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event2 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
+ assertThat(event2.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
}
@Test
@@ -199,10 +206,12 @@
// Check the correctness of the requests submitted by lock3A.
// One repeating request was sent to monitor the state of AE to get converged.
- captureSequenceProcessor.nextEvent().requestSequence
- // Once AE is converged, another repeatingrequest is sent to lock AE.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event1 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.isRepeating).isTrue()
+
+ // Once AE is converged, another repeating request is sent to lock AE.
+ val event2 = captureSequenceProcessor.nextEvent()
+ assertThat(event2.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
globalScope.launch {
listener3A.onRequestSequenceCreated(
@@ -228,9 +237,12 @@
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
// A single request to lock AF must have been used as well.
- val request2 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
+ val event3 = captureSequenceProcessor.nextEvent()
+ assertThat(event3.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
globalScope.cancel()
}
@@ -275,8 +287,8 @@
// For a new AE scan we first send a request to unlock AE just in case it was
// previously or internally locked.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(false)
+ val event1 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, false)
globalScope.launch {
listener3A.onRequestSequenceCreated(
@@ -302,14 +314,17 @@
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
// There should be one more request to lock AE after new scan is done.
- val request2 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event2 = captureSequenceProcessor.nextEvent()
+ assertThat(event2.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
// And one request to lock AF.
- val request3 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request3!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
- assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event3 = captureSequenceProcessor.nextEvent()
+ assertThat(event3.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
+ assertThat(event3.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
globalScope.cancel()
}
@@ -378,14 +393,17 @@
// There should be one request to monitor AF to finish it's scan.
captureSequenceProcessor.nextEvent()
// One request to lock AE
- val request2 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event2 = captureSequenceProcessor.nextEvent()
+ assertThat(event2.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
// And one request to lock AF.
- val request3 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request3!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
- assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event3 = captureSequenceProcessor.nextEvent()
+ assertThat(event3.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
+ assertThat(event3.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
globalScope.cancel()
}
@@ -451,21 +469,27 @@
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
// One request to cancel AF to start a new scan.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
+ val event1 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+ )
// There should be one request to monitor AF to finish it's scan.
captureSequenceProcessor.nextEvent()
// There should be one request to monitor lock AE.
- val request2 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event2 = captureSequenceProcessor.nextEvent()
+ assertThat(event2.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
// And one request to lock AF.
- val request3 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request3!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
- assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event3 = captureSequenceProcessor.nextEvent()
+ assertThat(event3.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
+ assertThat(event3.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
globalScope.cancel()
}
@@ -532,29 +556,26 @@
// There should be one request to monitor AF to finish it's scan.
val event = captureSequenceProcessor.nextEvent()
- assertThat(event.requestSequence!!.repeating).isTrue()
- assertThat(event.rejected).isFalse()
- assertThat(event.abort).isFalse()
- assertThat(event.close).isFalse()
- assertThat(event.submit).isTrue()
+ assertThat(event.isRepeating).isTrue()
- // One request to lock AE
+ // One request to lock AE (Repeating)
val request2Event = captureSequenceProcessor.nextEvent()
- assertThat(request2Event.requestSequence!!.repeating).isTrue()
- assertThat(request2Event.submit).isTrue()
- val request2 = request2Event.requestSequence!!
- assertThat(request2).isNotNull()
- assertThat(request2.requiredParameters).isNotEmpty()
- assertThat(request2.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ assertThat(request2Event.isRepeating).isTrue()
+ assertThat(request2Event.requests.size).isEqualTo(1)
+ assertThat(request2Event.requiredParameters)
+ .containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
// And one request to lock AF.
val request3Event = captureSequenceProcessor.nextEvent()
- assertThat(request3Event.requestSequence!!.repeating).isFalse()
- assertThat(request3Event.submit).isTrue()
- val request3 = request3Event.requestSequence!!
- assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
- assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ assertThat(request3Event.isCapture).isTrue()
+ assertThat(request3Event.requests.size).isEqualTo(1)
+ assertThat(request3Event.requiredParameters)
+ .containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
+ assertThat(request3Event.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
globalScope.cancel()
}
@@ -620,22 +641,29 @@
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
// One request to cancel AF to start a new scan.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
+ val event1 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+ )
+
// There should be one request to unlock AE and monitor the current AF scan to finish.
- val request2 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(false)
+ val event2 = captureSequenceProcessor.nextEvent()
+ assertThat(event2.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, false)
// There should be one request to monitor lock AE.
- val request3 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request3!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event3 = captureSequenceProcessor.nextEvent()
+ assertThat(event3.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
// And one request to lock AF.
- val request4 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request4!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
- assertThat(request4.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event4 = captureSequenceProcessor.nextEvent()
+ assertThat(event4.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
+ assertThat(event4.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
globalScope.cancel()
}
@@ -713,14 +741,17 @@
// We not check if the correct sequence of requests were submitted by lock3A call. The
// request should be a repeating request to lock AE.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event1 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
// The second request should be a single request to lock AF.
- val request2 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
- assertThat(request2.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event2 = captureSequenceProcessor.nextEvent()
+ assertThat(event2.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
+ assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
}
@Test
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASetTorchTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASetTorchTest.kt
index 7e4ff9a..5490270 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASetTorchTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASetTorchTest.kt
@@ -17,6 +17,7 @@
package androidx.camera.camera2.pipe.graph
import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH
import android.hardware.camera2.CaptureResult
import android.os.Build
import androidx.camera.camera2.pipe.AeMode
@@ -24,14 +25,12 @@
import androidx.camera.camera2.pipe.FrameNumber
import androidx.camera.camera2.pipe.RequestNumber
import androidx.camera.camera2.pipe.Result3A
-import androidx.camera.camera2.pipe.TorchState
import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
import androidx.camera.camera2.pipe.testing.FakeGraphProcessor
import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
import androidx.camera.camera2.pipe.testing.RobolectricCameraPipeTestRunner
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.junit.After
@@ -39,7 +38,6 @@
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
-@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(RobolectricCameraPipeTestRunner::class)
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
internal class Controller3ASetTorchTest {
@@ -56,7 +54,7 @@
}
@Test
- fun testSetTorchFailsImmediatelyWithoutRepeatingRequest() = runTest {
+ fun setTorchOn_withoutRepeatingRequest_failsImmediatelyWithNoGraphStateChange() = runTest {
val graphProcessor2 = FakeGraphProcessor()
val controller3A =
Controller3A(
@@ -65,17 +63,42 @@
graphProcessor2.graphState3A,
listener3A
)
- val result = controller3A.setTorch(TorchState.ON)
+ val result = controller3A.setTorchOn()
assertThat(result.await().status).isEqualTo(Result3A.Status.SUBMIT_FAILED)
assertThat(graphProcessor2.graphState3A.flashMode).isEqualTo(FlashMode.TORCH)
}
@Test
- fun testSetTorchOn() = runTest {
- val result = controller3A.setTorch(TorchState.ON)
+ fun setTorchOff_withoutRepeatingRequest_failsImmediatelyWithNoGraphStateChange() = runTest {
+ val graphProcessor2 = FakeGraphProcessor()
+ val controller3A =
+ Controller3A(
+ graphProcessor2,
+ FakeCameraMetadata(),
+ graphProcessor2.graphState3A,
+ listener3A
+ )
+ val result = controller3A.setTorchOff()
+ assertThat(result.await().status).isEqualTo(Result3A.Status.SUBMIT_FAILED)
+ assertThat(graphProcessor2.graphState3A.flashMode).isEqualTo(FlashMode.OFF)
+ }
+
+ @Test
+ fun setTorchOn_updatesGraphStateWithAeModeOnAndFlashModeTorch() = runTest {
+ controller3A.setTorchOn()
assertThat(graphState3A.aeMode!!.value).isEqualTo(CaptureRequest.CONTROL_AE_MODE_ON)
assertThat(graphState3A.flashMode!!.value).isEqualTo(CaptureRequest.FLASH_MODE_TORCH)
+ }
+
+ @Test
+ fun setTorchOn_noCaptureResultProvided_resultIncomplete() = runTest {
+ val result = controller3A.setTorchOn()
assertThat(result.isCompleted).isFalse()
+ }
+
+ @Test
+ fun setTorchOn_captureResultProvidedWithAeOnAndFlashTorch_returnsOkResult() = runTest {
+ val result = controller3A.setTorchOn()
launch {
listener3A.onRequestSequenceCreated(
@@ -94,17 +117,62 @@
)
)
}
+
val result3A = result.await()
assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
}
@Test
- fun testSetTorchOff() = runTest {
- val result = controller3A.setTorch(TorchState.OFF)
- assertThat(graphState3A.aeMode!!.value).isEqualTo(CaptureRequest.CONTROL_AE_MODE_ON)
+ fun setTorchOn_captureResultProvidedWithOnlyFlashTorch_resultIncomplete() = runTest {
+ val result = controller3A.setTorchOn()
+
+ launch {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(requestNumber = RequestNumber(1))
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata =
+ mapOf(CaptureResult.FLASH_MODE to CaptureResult.FLASH_MODE_TORCH)
+ )
+ )
+ }
+ .join()
+
+ assertThat(result.isCompleted).isFalse()
+ }
+
+ @Test
+ fun setTorchOff_updatesGraphStateWithFlashModeOff() = runTest {
+ controller3A.setTorchOff()
assertThat(graphState3A.flashMode!!.value).isEqualTo(CaptureRequest.FLASH_MODE_OFF)
+ }
+
+ @Test
+ fun setTorchOffWithoutAeMode_graphStateAeModeStaysNull() = runTest {
+ controller3A.setTorchOff()
+ assertThat(graphState3A.aeMode?.value).isNull() // null is default value here
+ }
+
+ @Test
+ fun setTorchOffWithAutoFlashAeMode_graphStateAeModeUpdatedToAutoFlash() = runTest {
+ controller3A.setTorchOff(aeMode = AeMode.ON_AUTO_FLASH)
+ assertThat(graphState3A.aeMode?.value).isEqualTo(CONTROL_AE_MODE_ON_AUTO_FLASH)
+ }
+
+ @Test
+ fun setTorchOff_noCaptureResultWithUpdatedStates_resultIncomplete() = runTest {
+ val result = controller3A.setTorchOff()
assertThat(result.isCompleted).isFalse()
+ }
+
+ @Test
+ fun setTorchOffWithoutAeMode_captureResultProvidedWithFlashOff_returnsOkResult() = runTest {
+ val result = controller3A.setTorchOff()
launch {
listener3A.onRequestSequenceCreated(
@@ -115,11 +183,7 @@
FrameNumber(101L),
FakeFrameMetadata(
frameNumber = FrameNumber(101L),
- resultMetadata =
- mapOf(
- CaptureResult.CONTROL_AE_MODE to CaptureResult.CONTROL_AE_MODE_ON,
- CaptureResult.FLASH_MODE to CaptureResult.FLASH_MODE_OFF
- )
+ resultMetadata = mapOf(CaptureResult.FLASH_MODE to CaptureResult.FLASH_MODE_OFF)
)
)
}
@@ -129,33 +193,90 @@
}
@Test
- fun testSetTorchDoesNotChangeAeModeIfNotNeeded() = runTest {
- graphState3A.update(aeMode = AeMode.OFF)
+ fun setTorchOffWithAutoFlashAe_captureResultProvidedWithOnlyFlashOff_resultIncomplete() =
+ runTest {
+ val result = controller3A.setTorchOff(aeMode = AeMode.ON_AUTO_FLASH)
- val result = controller3A.setTorch(TorchState.ON)
- assertThat(graphState3A.aeMode!!.value).isEqualTo(CaptureRequest.CONTROL_AE_MODE_OFF)
- assertThat(graphState3A.flashMode!!.value).isEqualTo(CaptureRequest.FLASH_MODE_TORCH)
- assertThat(result.isCompleted).isFalse()
-
- launch {
- listener3A.onRequestSequenceCreated(
- FakeRequestMetadata(requestNumber = RequestNumber(1))
- )
- listener3A.onPartialCaptureResult(
- FakeRequestMetadata(requestNumber = RequestNumber(1)),
- FrameNumber(101L),
- FakeFrameMetadata(
- frameNumber = FrameNumber(101L),
- resultMetadata =
- mapOf(
- CaptureResult.CONTROL_AE_MODE to CaptureResult.CONTROL_AE_MODE_OFF,
- CaptureResult.FLASH_MODE to CaptureResult.FLASH_MODE_TORCH
+ launch {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(requestNumber = RequestNumber(1))
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata =
+ mapOf(CaptureResult.FLASH_MODE to CaptureResult.FLASH_MODE_OFF)
)
- )
- )
+ )
+ }
+ .join()
+
+ assertThat(result.isCompleted).isFalse()
}
- val result3A = result.await()
- assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
- assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
- }
+
+ @Test
+ fun setTorchOffWithAutoFlashAe_captureResultProvidedWithAutoAeAndFlashOff_returnsOkResult() =
+ runTest {
+ val result = controller3A.setTorchOff(aeMode = AeMode.ON_AUTO_FLASH)
+
+ launch {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(requestNumber = RequestNumber(1))
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata =
+ mapOf(
+ CaptureResult.CONTROL_AE_MODE to
+ CaptureResult.CONTROL_AE_MODE_ON_AUTO_FLASH,
+ CaptureResult.FLASH_MODE to CaptureResult.FLASH_MODE_OFF
+ )
+ )
+ )
+ }
+ val result3A = result.await()
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
+ }
+
+ @Test
+ fun setTorchOn_graphStateAlreadyAeOffSoNoChangeNeeded_aeModeUnchangedButFlashChangedToTorch() =
+ runTest {
+ graphState3A.update(aeMode = AeMode.OFF)
+
+ controller3A.setTorchOn()
+ assertThat(graphState3A.aeMode!!.value).isEqualTo(CaptureRequest.CONTROL_AE_MODE_OFF)
+ assertThat(graphState3A.flashMode!!.value).isEqualTo(CaptureRequest.FLASH_MODE_TORCH)
+ }
+
+ @Test
+ fun setTorchOnWithGraphStateAlreadyAeOff_captureResultProvidedWithFlashTorch_returnsOkResult() =
+ runTest {
+ graphState3A.update(aeMode = AeMode.OFF)
+
+ val result = controller3A.setTorchOn()
+
+ launch {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(requestNumber = RequestNumber(1))
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata =
+ mapOf(CaptureResult.FLASH_MODE to CaptureResult.FLASH_MODE_TORCH)
+ )
+ )
+ }
+ val result3A = result.await()
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
+ }
}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUnlock3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUnlock3ATest.kt
index e70b7a7..9305a51 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUnlock3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUnlock3ATest.kt
@@ -25,6 +25,7 @@
import androidx.camera.camera2.pipe.RequestNumber
import androidx.camera.camera2.pipe.Result3A
import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.requiredParameters
import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
import androidx.camera.camera2.pipe.testing.FakeGraphProcessor
import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
@@ -104,8 +105,8 @@
assertThat(result.isCompleted).isFalse()
// There should be one request to lock AE.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(false)
+ val event1 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, false)
repeatingJob.cancel()
repeatingJob.join()
@@ -164,9 +165,12 @@
assertThat(result.isCompleted).isFalse()
// There should be one request to unlock AF.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
+ val event1 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+ )
repeatingJob.cancel()
repeatingJob.join()
@@ -225,8 +229,8 @@
assertThat(result.isCompleted).isFalse()
// There should be one request to lock AWB.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AWB_LOCK]).isEqualTo(false)
+ val event1 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AWB_LOCK, false)
repeatingJob.cancel()
repeatingJob.join()
@@ -287,12 +291,15 @@
assertThat(result.isCompleted).isFalse()
// There should be one request to unlock AF.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
+ val event1 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+ )
// Then request to unlock AE.
- val request2 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(false)
+ val event2 = captureSequenceProcessor.nextEvent()
+ assertThat(event2.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, false)
repeatingJob.cancel()
repeatingJob.join()
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphLoopTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphLoopTest.kt
index 728d859..711da09 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphLoopTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphLoopTest.kt
@@ -17,21 +17,25 @@
package androidx.camera.camera2.pipe.graph
import android.os.Build
-import android.view.Surface
import androidx.camera.camera2.pipe.CameraGraphId
-import androidx.camera.camera2.pipe.CameraId
-import androidx.camera.camera2.pipe.CaptureSequence
-import androidx.camera.camera2.pipe.CaptureSequenceProcessor
import androidx.camera.camera2.pipe.Request
import androidx.camera.camera2.pipe.Result3A
import androidx.camera.camera2.pipe.StreamId
import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
-import androidx.camera.camera2.pipe.testing.FakeCaptureSequence
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.defaultParameters
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isAbort
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isCapture
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isClose
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isRejected
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isRepeating
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isStopRepeating
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.requests
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.requiredParameters
import androidx.camera.camera2.pipe.testing.FakeMetadata.Companion.TEST_KEY
import androidx.camera.camera2.pipe.testing.FakeSurfaces
import androidx.testutils.assertThrows
import com.google.common.truth.Truth.assertThat
-import kotlinx.atomicfu.atomic
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
@@ -73,8 +77,10 @@
stream2 to fakeSurfaces.createFakeSurface()
)
- private val csp1 = SimpleCSP(fakeCameraId, surfaceMap)
- private val csp2 = SimpleCSP(fakeCameraId, surfaceMap)
+ private val csp1 =
+ FakeCaptureSequenceProcessor(fakeCameraId).also { it.surfaceMap = surfaceMap }
+ private val csp2 =
+ FakeCaptureSequenceProcessor(fakeCameraId).also { it.surfaceMap = surfaceMap }
private val grp1 = GraphRequestProcessor.from(csp1)
private val grp2 = GraphRequestProcessor.from(csp2)
@@ -823,90 +829,32 @@
assertThat(csp2.events[0].isClose).isTrue()
}
- private val SimpleCSP.SimpleCSPEvent.requests: List<Request>
- get() = (this as SimpleCSP.Submit).captureSequence.captureRequestList
+ @Test
+ fun settingRepeatingRequestWhenRequestsAreRejectedDoesNotAttemptMultipleRepeatingRequests() =
+ testScope.runTest {
+ // Arrange
+ csp1.rejectSubmit = true
+ graphLoop.requestProcessor = grp1
+ graphLoop.repeatingRequest = request1
+ advanceUntilIdle()
- private val SimpleCSP.SimpleCSPEvent.requiredParameters: Map<*, Any?>
- get() = (this as SimpleCSP.Submit).captureSequence.requiredParameters
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isRejected).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
- private val SimpleCSP.SimpleCSPEvent.defaultParameters: Map<*, Any?>
- get() = (this as SimpleCSP.Submit).captureSequence.defaultParameters
+ graphLoop.repeatingRequest = request2
+ advanceUntilIdle()
- private val SimpleCSP.SimpleCSPEvent.isRepeating: Boolean
- get() = (this as? SimpleCSP.Submit)?.captureSequence?.repeating ?: false
+ assertThat(csp1.events.size).isEqualTo(2)
+ assertThat(csp1.events[1].isRejected).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request2)
- private val SimpleCSP.SimpleCSPEvent.isCapture: Boolean
- get() = (this as? SimpleCSP.Submit)?.captureSequence?.repeating == false
+ csp1.rejectSubmit = false
+ graphLoop.invalidate()
+ advanceUntilIdle()
- private val SimpleCSP.SimpleCSPEvent.isAbort: Boolean
- get() = this is SimpleCSP.AbortCaptures
-
- private val SimpleCSP.SimpleCSPEvent.isStopRepeating: Boolean
- get() = this is SimpleCSP.StopRepeating
-
- private val SimpleCSP.SimpleCSPEvent.isClose: Boolean
- get() = this is SimpleCSP.Close
-
- internal class SimpleCSP(
- private val cameraId: CameraId,
- private val surfaceMap: Map<StreamId, Surface>
- ) : CaptureSequenceProcessor<Request, FakeCaptureSequence> {
- val events = mutableListOf<SimpleCSPEvent>()
- var throwOnBuild = false
- private var closed = false
- private val sequenceIds = atomic(0)
-
- override fun build(
- isRepeating: Boolean,
- requests: List<Request>,
- defaultParameters: Map<*, Any?>,
- requiredParameters: Map<*, Any?>,
- listeners: List<Request.Listener>,
- sequenceListener: CaptureSequence.CaptureSequenceListener
- ): FakeCaptureSequence? {
- if (closed) return null
- if (throwOnBuild) throw RuntimeException("Test Exception")
- return FakeCaptureSequence.create(
- cameraId = cameraId,
- repeating = isRepeating,
- requests = requests,
- surfaceMap = surfaceMap,
- defaultParameters = defaultParameters,
- requiredParameters = requiredParameters,
- listeners = listeners,
- sequenceListener = sequenceListener
- )
+ assertThat(csp1.events.size).isEqualTo(3)
+ assertThat(csp1.events[2].isRepeating).isTrue()
+ assertThat(csp1.events[2].requests).containsExactly(request2)
}
-
- override fun abortCaptures() {
- events.add(AbortCaptures)
- }
-
- override fun stopRepeating() {
- events.add(StopRepeating)
- }
-
- override suspend fun shutdown() {
- closed = true
- events.add(Close)
- }
-
- override fun submit(captureSequence: FakeCaptureSequence): Int? {
- if (!closed) {
- events.add(Submit(captureSequence))
- return sequenceIds.incrementAndGet()
- }
- return null
- }
-
- sealed class SimpleCSPEvent
-
- object Close : SimpleCSPEvent()
-
- object StopRepeating : SimpleCSPEvent()
-
- object AbortCaptures : SimpleCSPEvent()
-
- data class Submit(val captureSequence: FakeCaptureSequence) : SimpleCSPEvent()
- }
}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt
index 13e29b9..c648043 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt
@@ -28,7 +28,12 @@
import androidx.camera.camera2.pipe.Request
import androidx.camera.camera2.pipe.StreamId
import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor
-import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.awaitEvent
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isCapture
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isClose
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isRejected
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isRepeating
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.requests
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.requiredParameters
import androidx.camera.camera2.pipe.testing.FakeGraphConfigs
import androidx.camera.camera2.pipe.testing.FakeRequestListener
import androidx.camera.camera2.pipe.testing.FakeThreads
@@ -36,10 +41,9 @@
import androidx.testutils.assertThrows
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
-import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withTimeoutOrNull
@@ -52,17 +56,20 @@
@RunWith(RobolectricCameraPipeTestRunner::class)
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
internal class GraphProcessorTest {
+ private val testScope = TestScope()
+ private val fakeThreads = FakeThreads.fromTestScope(testScope)
+
private val globalListener = FakeRequestListener()
private val graphState3A = GraphState3A()
private val graphListener3A = Listener3A()
private val streamId = StreamId(0)
private val surfaceMap = mapOf(streamId to Surface(SurfaceTexture(1)))
- private val fakeProcessor1 = FakeCaptureSequenceProcessor().also { it.surfaceMap = surfaceMap }
- private val fakeProcessor2 = FakeCaptureSequenceProcessor().also { it.surfaceMap = surfaceMap }
+ private val csp1 = FakeCaptureSequenceProcessor().also { it.surfaceMap = surfaceMap }
+ private val csp2 = FakeCaptureSequenceProcessor().also { it.surfaceMap = surfaceMap }
- private val graphRequestProcessor1 = GraphRequestProcessor.from(fakeProcessor1)
- private val graphRequestProcessor2 = GraphRequestProcessor.from(fakeProcessor2)
+ private val grp1 = GraphRequestProcessor.from(csp1)
+ private val grp2 = GraphRequestProcessor.from(csp2)
private val requestListener1 = FakeRequestListener()
private val request1 = Request(listOf(StreamId(0)), listeners = listOf(requestListener1))
@@ -70,516 +77,375 @@
private val requestListener2 = FakeRequestListener()
private val request2 = Request(listOf(StreamId(0)), listeners = listOf(requestListener2))
+ private val graphProcessor =
+ GraphProcessorImpl(
+ fakeThreads,
+ CameraGraphId.nextId(),
+ FakeGraphConfigs.graphConfig,
+ graphState3A,
+ graphListener3A,
+ arrayListOf(globalListener)
+ )
+
@After
fun teardown() {
surfaceMap[streamId]?.release()
}
@Test
- fun graphProcessorSubmitsRequests() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.submit(request1)
- advanceUntilIdle()
+ fun graphProcessorSubmitsRequests() =
+ testScope.runTest {
+ graphProcessor.onGraphStarted(grp1)
+ graphProcessor.submit(request1)
+ advanceUntilIdle()
- // Make sure the requests get submitted to the request processor
- val event = fakeProcessor1.nextEvent()
- assertThat(event.requestSequence!!.captureRequestList).containsExactly(request1)
- assertThat(event.requestSequence!!.requiredParameters)
- .containsEntry(CaptureRequest.JPEG_THUMBNAIL_QUALITY, 42)
- }
+ // Make sure the requests get submitted to the request processor
+ assertThat(csp1.events.size).isEqualTo(1)
- @Test
- fun graphProcessorSubmitsRequestsToMostRecentProcessor() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
-
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.onGraphStarted(graphRequestProcessor2)
- graphProcessor.submit(request1)
-
- val event1 = fakeProcessor1.nextEvent()
- assertThat(event1.close).isTrue()
-
- val event2 = fakeProcessor2.nextEvent()
- assertThat(event2.submit).isTrue()
- assertThat(event2.requestSequence!!.captureRequestList).containsExactly(request1)
- }
-
- @Test
- fun graphProcessorSubmitsQueuedRequests() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
-
- graphProcessor.submit(request1)
- graphProcessor.submit(request2)
-
- // Request1 and 2 should be queued and will be submitted even when the request
- // processor is set after the requests are submitted.
- graphProcessor.onGraphStarted(graphRequestProcessor1)
-
- val event1 = fakeProcessor1.awaitEvent(request = request1) { it.submit }
- assertThat(event1.requestSequence!!.captureRequestList).hasSize(1)
- assertThat(event1.requestSequence!!.captureRequestList).contains(request1)
-
- val event2 = fakeProcessor1.nextEvent()
- assertThat(event2.requestSequence!!.captureRequestList).hasSize(1)
- assertThat(event2.requestSequence!!.captureRequestList).contains(request2)
- }
-
- @Test
- fun graphProcessorSubmitsBurstsOfRequestsTogetherWithExtras() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
-
- graphProcessor.submit(listOf(request1, request2))
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- val event = fakeProcessor1.awaitEvent(request = request1) { it.submit }
- assertThat(event.requestSequence!!.captureRequestList).hasSize(2)
- assertThat(event.requestSequence!!.captureRequestList).contains(request1)
- assertThat(event.requestSequence!!.captureRequestList).contains(request2)
- }
-
- @Test
- fun graphProcessorDoesNotForgetRejectedRequests() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
-
- fakeProcessor1.rejectRequests = true
- graphProcessor.onGraphStarted(graphRequestProcessor1)
-
- graphProcessor.submit(request1)
- val event1 = fakeProcessor1.nextEvent()
- assertThat(event1.rejected).isTrue()
- assertThat(event1.requestSequence!!.captureRequestList[0]).isSameInstanceAs(request1)
-
- graphProcessor.submit(request2)
- val event2 = fakeProcessor1.nextEvent()
- assertThat(event2.rejected).isTrue()
- assertThat(event2.requestSequence!!.captureRequestList[0]).isSameInstanceAs(request1)
-
- graphProcessor.onGraphStarted(graphRequestProcessor2)
- assertThat(fakeProcessor2.nextEvent().requestSequence!!.captureRequestList[0])
- .isSameInstanceAs(request1)
- assertThat(fakeProcessor2.nextEvent().requestSequence!!.captureRequestList[0])
- .isSameInstanceAs(request2)
- }
-
- @Test
- fun graphProcessorContinuesSubmittingRequestsWhenFirstRequestIsRejected() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
-
- // Note: setting the requestProcessor, and calling submit() can both trigger a call
- // to submit a request.
- fakeProcessor1.rejectRequests = true
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.submit(request1)
-
- // Check to make sure that submit is called at least once, and that request1 is rejected
- // from the request processor.
- fakeProcessor1.awaitEvent(request = request1) { it.rejected }
-
- // Stop rejecting requests
- fakeProcessor1.rejectRequests = false
-
- graphProcessor.submit(request2)
- // Cycle events until we get a submitted event with request1
- val event2 = fakeProcessor1.awaitEvent(request = request1) { it.submit }
- assertThat(event2.rejected).isFalse()
-
- // Assert that immediately after we get a successfully submitted request, the
- // next request is also submitted.
- val event3 = fakeProcessor1.nextEvent()
- assertThat(event3.requestSequence!!.captureRequestList).contains(request2)
- assertThat(event3.submit).isTrue()
- assertThat(event3.rejected).isFalse()
- }
-
- @Test
- fun graphProcessorSetsRepeatingRequest() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
-
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.repeatingRequest = request1
- graphProcessor.repeatingRequest = request2
- advanceUntilIdle()
-
- val event =
- fakeProcessor1.awaitEvent(request = request2) {
- it.submit && it.requestSequence?.repeating == true
- }
- assertThat(event.requestSequence!!.requiredParameters)
- .containsEntry(CaptureRequest.JPEG_THUMBNAIL_QUALITY, 42)
- }
-
- @Test
- fun graphProcessorDoesNotForgetRejectedRepeatingRequests() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
-
- fakeProcessor1.rejectRequests = true
- graphProcessor.onGraphStarted(graphRequestProcessor1)
-
- graphProcessor.repeatingRequest = request1
- val event1 = fakeProcessor1.nextEvent()
- assertThat(event1.rejected).isTrue()
- assertThat(event1.requestSequence!!.captureRequestList[0]).isSameInstanceAs(request1)
-
- graphProcessor.repeatingRequest = request2
- val event2 = fakeProcessor1.nextEvent()
- assertThat(event2.rejected).isTrue()
- fakeProcessor1.awaitEvent(request = request2) {
- !it.submit && it.requestSequence?.repeating == true
+ assertThat(csp1.events[0].isCapture).isTrue()
+ assertThat(csp1.events[0].requiredParameters)
+ .containsEntry(CaptureRequest.JPEG_THUMBNAIL_QUALITY, 42)
}
- fakeProcessor1.rejectRequests = false
- graphProcessor.invalidate()
-
- fakeProcessor1.awaitEvent(request = request2) {
- it.submit && it.requestSequence?.repeating == true
- }
- }
-
@Test
- fun graphProcessorTracksRepeatingRequest() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
+ fun graphProcessorSubmitsRequestsToMostRecentProcessor() =
+ testScope.runTest {
+ graphProcessor.onGraphStarted(grp1)
+ graphProcessor.onGraphStarted(grp2)
+ graphProcessor.submit(request1)
+ advanceUntilIdle()
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.repeatingRequest = request1
- advanceUntilIdle()
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isClose).isTrue()
- fakeProcessor1.awaitEvent(request = request1) {
- it.submit && it.requestSequence?.repeating == true
+ assertThat(csp2.events.size).isEqualTo(1)
+ assertThat(csp2.events[0].isCapture).isTrue()
+ assertThat(csp2.events[0].requests).containsExactly(request1)
}
- graphProcessor.onGraphStarted(graphRequestProcessor2)
- advanceUntilIdle()
+ @Test
+ fun graphProcessorSubmitsQueuedRequests() =
+ testScope.runTest {
+ graphProcessor.submit(request1)
+ graphProcessor.submit(request2)
- fakeProcessor2.awaitEvent(request = request1) {
- it.submit && it.requestSequence?.repeating == true
+ // Request1 and 2 should be queued and will be submitted even when the request
+ // processor is set after the requests are submitted.
+ graphProcessor.onGraphStarted(grp1)
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(2)
+ assertThat(csp1.events[0].isCapture).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
+
+ assertThat(csp1.events[1].isCapture).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request2)
}
- }
@Test
- fun graphProcessorTracksRejectedRepeatingRequests() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
+ fun graphProcessorSubmitsBurstsOfRequestsTogetherWithExtras() =
+ testScope.runTest {
+ graphProcessor.submit(listOf(request1, request2))
+ graphProcessor.onGraphStarted(grp1)
+ advanceUntilIdle()
- fakeProcessor1.rejectRequests = true
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.repeatingRequest = request1
- fakeProcessor1.awaitEvent(request = request1) { it.rejected }
-
- graphProcessor.onGraphStarted(graphRequestProcessor2)
- fakeProcessor2.awaitEvent(request = request1) {
- it.submit && it.requestSequence?.repeating == true
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isCapture).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1, request2).inOrder()
}
- }
@Test
- fun graphProcessorSubmitsRepeatingRequestAndQueuedRequests() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
+ fun graphProcessorDoesNotForgetRejectedRequests() =
+ testScope.runTest {
+ csp1.rejectSubmit = true
+ graphProcessor.onGraphStarted(grp1)
+ graphProcessor.submit(request1)
+ advanceUntilIdle()
- graphProcessor.repeatingRequest = request1
- graphProcessor.submit(request2)
- delay(50)
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isRejected).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
- graphProcessor.onGraphStarted(graphRequestProcessor1)
+ graphProcessor.submit(request2)
+ advanceUntilIdle()
+ assertThat(csp1.events.size).isEqualTo(2)
+ assertThat(csp1.events[1].isRejected).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request1) // Re-attempt #1
- var hasRequest1Event = false
- var hasRequest2Event = false
+ graphProcessor.onGraphStarted(grp2)
+ advanceUntilIdle()
- // Loop until we see at least one repeating request, and one submit event.
- launch {
- while (!hasRequest1Event && !hasRequest2Event) {
- val event = fakeProcessor1.nextEvent()
- hasRequest1Event =
- hasRequest1Event ||
- event.requestSequence?.captureRequestList?.contains(request1) ?: false
- hasRequest2Event =
- hasRequest2Event ||
- event.requestSequence?.captureRequestList?.contains(request2) ?: false
- }
- }
- .join()
- }
+ // Assert that after a new request processor is set, it receives the queued up requests.
+ assertThat(csp2.events.size).isEqualTo(2)
+ assertThat(csp2.events[0].isCapture).isTrue()
+ assertThat(csp2.events[0].requests).containsExactly(request1)
+ assertThat(csp2.events[1].isCapture).isTrue()
+ assertThat(csp2.events[1].requests).containsExactly(request2).inOrder()
+ }
@Test
- fun graphProcessorAbortsQueuedRequests() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
+ fun graphProcessorContinuesSubmittingRequestsWhenFirstRequestIsRejected() =
+ testScope.runTest {
- graphProcessor.repeatingRequest = request1
- graphProcessor.submit(request2)
+ // Note: setting the requestProcessor, and calling submit() can both trigger a call
+ // to submit a request.
+ csp1.rejectSubmit = true
+ graphProcessor.onGraphStarted(grp1)
+ graphProcessor.submit(request1)
+ advanceUntilIdle()
- // Abort queued and in-flight requests.
- graphProcessor.abort()
- graphProcessor.onGraphStarted(graphRequestProcessor1)
+ // Check to make sure that submit is called at least once, and that request1 is rejected
+ // from the request processor.
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isRejected).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
- val abortEvent1 =
- withTimeoutOrNull(timeMillis = 50L) { requestListener1.onAbortedFlow.firstOrNull() }
- val abortEvent2 = requestListener2.onAbortedFlow.first()
- val globalAbortEvent = globalListener.onAbortedFlow.first()
+ // Stop rejecting requests
+ csp1.rejectSubmit = false
- assertThat(abortEvent1).isNull()
- assertThat(abortEvent2.request).isSameInstanceAs(request2)
- assertThat(globalAbortEvent.request).isSameInstanceAs(request2)
+ graphProcessor.submit(request2)
+ advanceUntilIdle()
- val nextSequence = fakeProcessor1.nextRequestSequence()
- assertThat(nextSequence.captureRequestList.first()).isSameInstanceAs(request1)
- assertThat(nextSequence.requestMetadata[request1]!!.repeating).isTrue()
- }
+ // Assert that immediately after we get a successfully submitted request, the
+ // next request is also submitted.
+ assertThat(csp1.events.size).isEqualTo(3)
+ assertThat(csp1.events[1].isCapture).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request1)
+ assertThat(csp1.events[2].isCapture).isTrue()
+ assertThat(csp1.events[2].requests).containsExactly(request2)
+ }
@Test
- fun closingGraphProcessorAbortsSubsequentRequests() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
- graphProcessor.close()
- advanceUntilIdle()
+ fun graphProcessorSetsRepeatingRequest() =
+ testScope.runTest {
+ graphProcessor.onGraphStarted(grp1)
+ graphProcessor.repeatingRequest = request1
+ graphProcessor.repeatingRequest = request2
+ advanceUntilIdle()
- // Abort queued and in-flight requests.
- // graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.repeatingRequest = request1
- graphProcessor.submit(request2)
-
- val abortEvent1 =
- withTimeoutOrNull(timeMillis = 50L) { requestListener1.onAbortedFlow.firstOrNull() }
- val abortEvent2 = requestListener2.onAbortedFlow.first()
- assertThat(abortEvent1).isNull()
- assertThat(abortEvent2.request).isSameInstanceAs(request2)
- }
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isRepeating).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request2)
+ assertThat(csp1.events[0].requiredParameters)
+ .containsEntry(CaptureRequest.JPEG_THUMBNAIL_QUALITY, 42)
+ }
@Test
- fun graphProcessorResubmitsParametersAfterGraphStarts() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
+ fun graphProcessorDoesNotForgetRejectedRepeatingRequests() =
+ testScope.runTest {
+ csp1.rejectSubmit = true
+ graphProcessor.onGraphStarted(grp1)
+ graphProcessor.repeatingRequest = request1
+ advanceUntilIdle()
- // Submit a repeating request first to make sure we have one in progress.
- graphProcessor.repeatingRequest = request1
- advanceUntilIdle()
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isRejected).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
- graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to false))
- advanceUntilIdle()
+ graphProcessor.repeatingRequest = request2
+ advanceUntilIdle()
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- advanceUntilIdle()
- val event1 = fakeProcessor1.nextEvent()
- assertThat(event1.requestSequence?.repeating).isTrue()
- val event2 = fakeProcessor1.nextEvent()
- assertThat(event2.requestSequence?.repeating).isFalse()
- assertThat(event2.requestSequence?.requestMetadata?.get(request1)?.get(CONTROL_AE_LOCK))
- .isFalse()
- }
+ assertThat(csp1.events.size).isEqualTo(2)
+ assertThat(csp1.events[1].isRejected).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request2)
+
+ csp1.rejectSubmit = false
+ graphProcessor.invalidate()
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(3)
+ assertThat(csp1.events[2].isRepeating).isTrue()
+ assertThat(csp1.events[2].requests).containsExactly(request2)
+ }
@Test
- fun graphProcessorSubmitsLatestParametersWhenSubmittedTwiceBeforeGraphStarts() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
+ fun graphProcessorTracksRepeatingRequest() =
+ testScope.runTest {
+ graphProcessor.onGraphStarted(grp1)
+ graphProcessor.repeatingRequest = request1
+ advanceUntilIdle()
- // Submit a repeating request first to make sure we have one in progress.
- graphProcessor.repeatingRequest = request1
- graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to false))
- graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to true))
- advanceUntilIdle()
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isRepeating).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- advanceUntilIdle()
+ graphProcessor.onGraphStarted(grp2)
+ advanceUntilIdle()
- val event1 = fakeProcessor1.nextEvent()
- assertThat(event1.requestSequence?.repeating).isTrue()
- val event2 = fakeProcessor1.nextEvent()
- assertThat(event2.requestSequence?.repeating).isFalse()
- assertThat(event2.requestSequence?.requestMetadata?.get(request1)?.get(CONTROL_AE_LOCK))
- .isFalse()
- val event3 = fakeProcessor1.nextEvent()
- assertThat(event3.requestSequence?.repeating).isFalse()
- assertThat(event3.requestSequence?.requestMetadata?.get(request1)?.get(CONTROL_AE_LOCK))
- .isTrue()
- }
+ assertThat(csp2.events.size).isEqualTo(1)
+ assertThat(csp2.events[0].isRepeating).isTrue()
+ assertThat(csp2.events[0].requests).containsExactly(request1)
+ }
@Test
- fun trySubmitShouldReturnFalseWhenNoRepeatingRequestIsQueued() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
+ fun graphProcessorTracksRejectedRepeatingRequests() =
+ testScope.runTest {
+ csp1.rejectSubmit = true
+ graphProcessor.onGraphStarted(grp1)
+ graphProcessor.repeatingRequest = request1
+ advanceUntilIdle()
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- advanceUntilIdle()
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isRejected).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
- assertThrows<IllegalStateException> {
+ graphProcessor.onGraphStarted(grp2)
+ advanceUntilIdle()
+
+ assertThat(csp2.events.size).isEqualTo(1)
+ assertThat(csp2.events[0].isRepeating).isTrue()
+ assertThat(csp2.events[0].requests).containsExactly(request1)
+ }
+
+ @Test
+ fun graphProcessorSubmitsRepeatingRequestAndQueuedRequests() =
+ testScope.runTest {
+ graphProcessor.repeatingRequest = request1
+ graphProcessor.submit(request2)
+ advanceUntilIdle()
+
+ graphProcessor.onGraphStarted(grp1)
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(2)
+ assertThat(csp1.events[0].isRepeating).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
+ assertThat(csp1.events[1].isCapture).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request2)
+ }
+
+ @Test
+ fun graphProcessorAbortsQueuedRequests() =
+ testScope.runTest {
+ graphProcessor.repeatingRequest = request1
+ graphProcessor.submit(request2)
+
+ // Abort queued and in-flight requests.
+ graphProcessor.abort()
+ graphProcessor.onGraphStarted(grp1)
+
+ val abortEvent1 = requestListener2.onAbortedFlow.first()
+ val globalAbortEvent = globalListener.onAbortedFlow.first()
+
+ assertThat(abortEvent1.request).isSameInstanceAs(request2)
+ assertThat(globalAbortEvent.request).isSameInstanceAs(request2)
+
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isRepeating).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
+ }
+
+ @Test
+ fun closingGraphProcessorAbortsSubsequentRequests() =
+ testScope.runTest {
+ graphProcessor.close()
+ advanceUntilIdle()
+
+ // Abort queued and in-flight requests.
+ // graphProcessor.onGraphStarted(graphRequestProcessor1)
+ graphProcessor.repeatingRequest = request1
+ graphProcessor.submit(request2)
+
+ val abortEvent1 =
+ withTimeoutOrNull(timeMillis = 50L) { requestListener1.onAbortedFlow.firstOrNull() }
+ val abortEvent2 = requestListener2.onAbortedFlow.first()
+ assertThat(abortEvent1).isNull()
+ assertThat(abortEvent2.request).isSameInstanceAs(request2)
+ }
+
+ @Test
+ fun graphProcessorResubmitsParametersAfterGraphStarts() =
+ testScope.runTest {
+ // Submit a repeating request first to make sure we have one in progress.
+ graphProcessor.repeatingRequest = request1
+ graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to false))
+ graphProcessor.onGraphStarted(grp1)
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(2)
+ assertThat(csp1.events[0].isRepeating).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
+ assertThat(csp1.events[1].isCapture).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request1)
+ assertThat(csp1.events[1].requiredParameters).containsEntry(CONTROL_AE_LOCK, false)
+ }
+
+ @Test
+ fun graphProcessorSubmitsLatestParametersWhenSubmittedTwiceBeforeGraphStarts() =
+ testScope.runTest {
+
+ // Submit a repeating request first to make sure we have one in progress.
+ graphProcessor.repeatingRequest = request1
+ graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to false))
graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to true))
+ advanceUntilIdle()
+
+ graphProcessor.onGraphStarted(grp1)
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(3)
+ assertThat(csp1.events[0].isRepeating).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
+
+ assertThat(csp1.events[1].isCapture).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request1)
+ assertThat(csp1.events[1].requiredParameters).containsEntry(CONTROL_AE_LOCK, false)
+
+ assertThat(csp1.events[2].isCapture).isTrue()
+ assertThat(csp1.events[2].requests).containsExactly(request1)
+ assertThat(csp1.events[2].requiredParameters).containsEntry(CONTROL_AE_LOCK, true)
}
- }
@Test
- fun graphProcessorChangesGraphStateOnError() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
- assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
+ fun trySubmitShouldReturnFalseWhenNoRepeatingRequestIsQueued() =
+ testScope.runTest {
+ graphProcessor.onGraphStarted(grp1)
+ advanceUntilIdle()
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.onGraphError(
- GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true)
- )
- assertThat(graphProcessor.graphState.value).isInstanceOf(GraphStateError::class.java)
- }
+ assertThrows<IllegalStateException> {
+ graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to true))
+ }
+ }
@Test
- fun graphProcessorDropsStaleErrors() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
+ fun graphProcessorChangesGraphStateOnError() =
+ testScope.runTest {
+ assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
+
+ graphProcessor.onGraphStarted(grp1)
+ graphProcessor.onGraphError(
+ GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true)
)
- assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
+ assertThat(graphProcessor.graphState.value).isInstanceOf(GraphStateError::class.java)
+ }
- graphProcessor.onGraphError(
- GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true)
- )
- assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
+ @Test
+ fun graphProcessorDropsStaleErrors() =
+ testScope.runTest {
+ assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
- graphProcessor.onGraphStarting()
- graphProcessor.onGraphStarted(graphRequestProcessor1)
+ graphProcessor.onGraphError(
+ GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true)
+ )
+ assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
- // GraphProcessor should drop errors while the camera graph is stopping.
- graphProcessor.onGraphStopping()
- graphProcessor.onGraphError(
- GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true)
- )
- assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
+ graphProcessor.onGraphStarting()
+ graphProcessor.onGraphStarted(grp1)
- // GraphProcessor should also drop errors while the camera graph is stopped.
- graphProcessor.onGraphStopped(graphRequestProcessor1)
- graphProcessor.onGraphError(
- GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true)
- )
- assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
- }
+ // GraphProcessor should drop errors while the camera graph is stopping.
+ graphProcessor.onGraphStopping()
+ graphProcessor.onGraphError(
+ GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true)
+ )
+ assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
+
+ // GraphProcessor should also drop errors while the camera graph is stopped.
+ graphProcessor.onGraphStopped(grp1)
+ graphProcessor.onGraphError(
+ GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true)
+ )
+ assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
+ }
}
diff --git a/camera/camera-camera2/build.gradle b/camera/camera-camera2/build.gradle
index 0401f79..fe5ef3d 100644
--- a/camera/camera-camera2/build.gradle
+++ b/camera/camera-camera2/build.gradle
@@ -63,8 +63,8 @@
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.truth)
androidTestImplementation(libs.testUiautomator)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it's own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it's own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation("androidx.appcompat:appcompat:1.1.0")
androidTestImplementation(project(":camera:camera-testing")) {
// Ensure camera-testing does not pull in androidx.test dependencies
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatBaseImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatBaseImpl.java
index ec4cc78..ace8b46 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatBaseImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatBaseImpl.java
@@ -45,8 +45,8 @@
// StreamConfigurationMap provided by Robolectric.
try {
return mStreamConfigurationMap.getOutputFormats();
- } catch (NullPointerException e) {
- Logger.e(TAG, "Failed to get output formats from StreamConfigurationMap", e);
+ } catch (NullPointerException | IllegalArgumentException e) {
+ Logger.w(TAG, "Failed to get output formats from StreamConfigurationMap", e);
return null;
}
}
diff --git a/camera/camera-core/build.gradle b/camera/camera-core/build.gradle
index 0cc031d..4efb36c 100644
--- a/camera/camera-core/build.gradle
+++ b/camera/camera-core/build.gradle
@@ -71,8 +71,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.truth)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it's own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it's own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(project(":camera:camera-testing")) {
// Ensure camera-testing does not pull in androidx.test dependencies
exclude(group:"androidx.test")
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
index 263b7a5..b596010 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
@@ -124,7 +124,7 @@
FakeCameraControl fakeCameraControl =
getCameraControlImplementation(mCameraUseCaseAdapter.getCameraControl());
- fakeCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+ fakeCameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
// Notify the cancel after the capture request has been successfully submitted
fakeCameraControl.notifyAllRequestsOnCaptureCancelled();
});
@@ -154,7 +154,7 @@
ImageCapture.OnImageCapturedCallback.class);
FakeCameraControl fakeCameraControl =
getCameraControlImplementation(mCameraUseCaseAdapter.getCameraControl());
- fakeCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+ fakeCameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
// Notify the failure after the capture request has been successfully submitted
fakeCameraControl.notifyAllRequestsOnCaptureFailed();
});
@@ -302,7 +302,7 @@
getCameraControlImplementation(mCameraUseCaseAdapter.getCameraControl());
// Simulates the case that the capture request failed after running in 300 ms.
- fakeCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+ fakeCameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
CameraXExecutors.mainThreadExecutor().schedule(() -> {
fakeCameraControl.notifyAllRequestsOnCaptureFailed();
}, 300, TimeUnit.MILLISECONDS);
@@ -395,7 +395,7 @@
getCameraControlImplementation(mCameraUseCaseAdapter.getCameraControl());
FakeCameraControl.OnNewCaptureRequestListener mockCaptureRequestListener =
mock(FakeCameraControl.OnNewCaptureRequestListener.class);
- fakeCameraControl.setOnNewCaptureRequestListener(mockCaptureRequestListener);
+ fakeCameraControl.addOnNewCaptureRequestListener(mockCaptureRequestListener);
// Act.
mInstrumentation.runOnMainSync(
@@ -463,7 +463,7 @@
FakeCameraControl fakeCameraControl =
getCameraControlImplementation(mCameraUseCaseAdapter.getCameraControl());
CountDownLatch latch = new CountDownLatch(1);
- fakeCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+ fakeCameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
latch.countDown();
});
@@ -492,7 +492,7 @@
private void addExtraFailureNotificationsForRetry(FakeCameraControl cameraControl,
int retryCount) {
if (retryCount > 0) {
- cameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+ cameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
addExtraFailureNotificationsForRetry(cameraControl, retryCount - 1);
cameraControl.notifyAllRequestsOnCaptureFailed();
});
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index 20b2ecd..0873c4b 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -1312,7 +1312,8 @@
if (mTakePictureManager == null) {
// mTakePictureManager is reused when the Surface is reset.
- mTakePictureManager = new TakePictureManager(mImageCaptureControl);
+ mTakePictureManager = getCurrentConfig().getTakePictureManagerProvider().newInstance(
+ mImageCaptureControl);
}
mTakePictureManager.setImagePipeline(mImagePipeline);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCaptureExt.kt b/camera/camera-core/src/main/java/androidx/camera/core/ImageCaptureExt.kt
index b09fe43..b46c236 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCaptureExt.kt
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCaptureExt.kt
@@ -162,7 +162,7 @@
}
override fun onCaptureSuccess(imageProxy: ImageProxy) {
- delegate?.onCaptureSuccess(imageProxy)
+ delegate?.onCaptureSuccess(imageProxy) ?: run { imageProxy.close() }
}
override fun onError(exception: ImageCaptureException) {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java
index b81133b..62245fd 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java
@@ -49,10 +49,11 @@
public final class ImageProcessingUtil {
private static final String TAG = "ImageProcessingUtil";
+ public static final String JNI_LIB_NAME = "image_processing_util_jni";
private static int sImageCount = 0;
static {
- System.loadLibrary("image_processing_util_jni");
+ System.loadLibrary(JNI_LIB_NAME);
}
enum Result {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Bitmap2JpegBytes.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Bitmap2JpegBytes.java
index d4ab73e..ceec2f8 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Bitmap2JpegBytes.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Bitmap2JpegBytes.java
@@ -24,6 +24,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.processing.Operation;
import androidx.camera.core.processing.Packet;
@@ -37,7 +38,8 @@
*
* <p>The {@link Bitmap} will be recycled and should not be used after the processing.
*/
-class Bitmap2JpegBytes implements Operation<Bitmap2JpegBytes.In, Packet<byte[]>> {
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class Bitmap2JpegBytes implements Operation<Bitmap2JpegBytes.In, Packet<byte[]>> {
@NonNull
@Override
@@ -79,16 +81,16 @@
* Input of {@link Bitmap2JpegBytes} processor.
*/
@AutoValue
- abstract static class In {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public abstract static class In {
abstract Packet<Bitmap> getPacket();
abstract int getJpegQuality();
@NonNull
- static In of(@NonNull Packet<Bitmap> imagePacket, int jpegQuality) {
+ public static In of(@NonNull Packet<Bitmap> imagePacket, int jpegQuality) {
return new AutoValue_Bitmap2JpegBytes_In(imagePacket, jpegQuality);
}
}
}
-
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/JpegBytes2Disk.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/JpegBytes2Disk.java
index 4dccd13..cb378f1 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/JpegBytes2Disk.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/JpegBytes2Disk.java
@@ -27,6 +27,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.impl.utils.Exif;
@@ -48,7 +49,9 @@
/**
* Saves JPEG bytes to disk.
*/
-class JpegBytes2Disk implements Operation<JpegBytes2Disk.In, ImageCapture.OutputFileResults> {
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class JpegBytes2Disk implements
+ Operation<JpegBytes2Disk.In, ImageCapture.OutputFileResults> {
private static final String TEMP_FILE_PREFIX = "CameraX";
private static final String TEMP_FILE_SUFFIX = ".tmp";
@@ -287,7 +290,8 @@
* Input packet.
*/
@AutoValue
- abstract static class In {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public abstract static class In {
@NonNull
abstract Packet<byte[]> getPacket();
@@ -296,7 +300,7 @@
abstract ImageCapture.OutputFileOptions getOutputFileOptions();
@NonNull
- static In of(@NonNull Packet<byte[]> jpegBytes,
+ public static In of(@NonNull Packet<byte[]> jpegBytes,
@NonNull ImageCapture.OutputFileOptions outputFileOptions) {
return new AutoValue_JpegBytes2Disk_In(jpegBytes, outputFileOptions);
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java
index fdf33ed..5b06259 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java
@@ -16,36 +16,15 @@
package androidx.camera.core.imagecapture;
-import static androidx.camera.core.ImageCapture.ERROR_CAMERA_CLOSED;
-import static androidx.camera.core.ImageCapture.ERROR_CAPTURE_FAILED;
-import static androidx.camera.core.impl.utils.Threads.checkMainThread;
-import static androidx.camera.core.impl.utils.executor.CameraXExecutors.directExecutor;
-import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
-import static androidx.core.util.Preconditions.checkState;
-
-import static java.util.Objects.requireNonNull;
-
-import android.util.Log;
-
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
-import androidx.camera.core.ForwardingImageProxy.OnImageCloseListener;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
-import androidx.camera.core.ImageProxy;
-import androidx.camera.core.Logger;
-import androidx.camera.core.impl.utils.futures.FutureCallback;
-import androidx.camera.core.impl.utils.futures.Futures;
-import androidx.core.util.Pair;
import com.google.auto.value.AutoValue;
-import com.google.common.util.concurrent.ListenableFuture;
-import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.Deque;
import java.util.List;
/**
@@ -62,46 +41,13 @@
*
* <p>The thread safety is guaranteed by using the main thread.
*/
-public class TakePictureManager implements OnImageCloseListener, TakePictureRequest.RetryControl {
-
- private static final String TAG = "TakePictureManager";
-
- // Queue of new requests that have not been sent to the pipeline/camera.
- @VisibleForTesting
- final Deque<TakePictureRequest> mNewRequests = new ArrayDeque<>();
- final ImageCaptureControl mImageCaptureControl;
- ImagePipeline mImagePipeline;
-
- // The current request being processed by the camera. Only one request can be processed by
- // the camera at the same time. Null if the camera is idle.
- @Nullable
- private RequestWithCallback mCapturingRequest;
- // The current requests that have not received a result or an error.
- private final List<RequestWithCallback> mIncompleteRequests;
-
- // Once paused, the class waits until the class is resumed to handle new requests.
- boolean mPaused = false;
-
- /**
- * @param imageCaptureControl for controlling {@link ImageCapture}
- */
- @MainThread
- public TakePictureManager(@NonNull ImageCaptureControl imageCaptureControl) {
- checkMainThread();
- mImageCaptureControl = imageCaptureControl;
- mIncompleteRequests = new ArrayList<>();
- }
-
+public interface TakePictureManager {
/**
* Sets the {@link ImagePipeline} for building capture requests and post-processing camera
* output.
*/
@MainThread
- public void setImagePipeline(@NonNull ImagePipeline imagePipeline) {
- checkMainThread();
- mImagePipeline = imagePipeline;
- mImagePipeline.setOnImageCloseListener(this);
- }
+ void setImagePipeline(@NonNull ImagePipeline imagePipeline);
/**
* Adds requests to the queue.
@@ -109,201 +55,52 @@
* <p>The requests in the queue will be executed based on the order being added.
*/
@MainThread
- public void offerRequest(@NonNull TakePictureRequest takePictureRequest) {
- checkMainThread();
- mNewRequests.offer(takePictureRequest);
- issueNextRequest();
- }
-
- @MainThread
- @Override
- public void retryRequest(@NonNull TakePictureRequest request) {
- checkMainThread();
- Logger.d(TAG, "Add a new request for retrying.");
- // Insert the request to the front of the queue.
- mNewRequests.addFirst(request);
- // Try to issue the newly added request in case condition allows.
- issueNextRequest();
- }
+ void offerRequest(@NonNull TakePictureRequest takePictureRequest);
/**
* Pauses sending request to camera.
*/
@MainThread
- public void pause() {
- checkMainThread();
- mPaused = true;
-
- // Always retry because the camera may not send an error callback during the reset.
- if (mCapturingRequest != null) {
- mCapturingRequest.abortSilentlyAndRetry();
- }
- }
+ void pause();
/**
* Resumes sending request to camera.
*/
@MainThread
- public void resume() {
- checkMainThread();
- mPaused = false;
- issueNextRequest();
- }
+ void resume();
/**
* Clears the requests queue.
*/
@MainThread
- public void abortRequests() {
- checkMainThread();
- ImageCaptureException exception =
- new ImageCaptureException(ERROR_CAMERA_CLOSED, "Camera is closed.", null);
-
- // Clear pending request first so aborting in-flight request won't trigger another capture.
- for (TakePictureRequest request : mNewRequests) {
- request.onError(exception);
- }
- mNewRequests.clear();
-
- // Abort the in-flight request after clearing the pending requests.
- // Snapshot to avoid concurrent modification with the removal in getCompleteFuture().
- List<RequestWithCallback> requestsSnapshot = new ArrayList<>(mIncompleteRequests);
- for (RequestWithCallback request : requestsSnapshot) {
- // TODO: optimize the performance by not processing aborted requests.
- request.abortAndSendErrorToApp(exception);
- }
- }
+ void abortRequests();
/**
- * Issues the next request if conditions allow.
+ * Returns whether any capture request is being processed currently.
*/
- @MainThread
- void issueNextRequest() {
- checkMainThread();
- Log.d(TAG, "Issue the next TakePictureRequest.");
- if (hasCapturingRequest()) {
- Log.d(TAG, "There is already a request in-flight.");
- return;
- }
- if (mPaused) {
- Log.d(TAG, "The class is paused.");
- return;
- }
- if (mImagePipeline.getCapacity() == 0) {
- Log.d(TAG, "Too many acquire images. Close image to be able to process next.");
- return;
- }
- TakePictureRequest request = mNewRequests.poll();
- if (request == null) {
- Log.d(TAG, "No new request.");
- return;
- }
-
- RequestWithCallback requestWithCallback = new RequestWithCallback(request, this);
- trackCurrentRequests(requestWithCallback);
-
- // Send requests.
- Pair<CameraRequest, ProcessingRequest> requests =
- mImagePipeline.createRequests(request, requestWithCallback,
- requestWithCallback.getCaptureFuture());
- CameraRequest cameraRequest = requireNonNull(requests.first);
- ProcessingRequest processingRequest = requireNonNull(requests.second);
- mImagePipeline.submitProcessingRequest(processingRequest);
- ListenableFuture<Void> captureRequestFuture = submitCameraRequest(cameraRequest);
- requestWithCallback.setCaptureRequestFuture(captureRequestFuture);
- }
-
- /**
- * Waits for the request to finish before issuing the next.
- */
- private void trackCurrentRequests(@NonNull RequestWithCallback requestWithCallback) {
- checkState(!hasCapturingRequest());
- mCapturingRequest = requestWithCallback;
-
- // Waits for the capture to finish before issuing the next.
- mCapturingRequest.getCaptureFuture().addListener(() -> {
- mCapturingRequest = null;
- issueNextRequest();
- }, directExecutor());
-
- // Track all incomplete requests so we can abort them when UseCase is detached.
- mIncompleteRequests.add(requestWithCallback);
- requestWithCallback.getCompleteFuture().addListener(() -> {
- mIncompleteRequests.remove(requestWithCallback);
- }, directExecutor());
- }
-
- /**
- * Submit a request to camera and post-processing pipeline.
- *
- * <p>Flash is locked/unlocked during the flight of a {@link CameraRequest}.
- */
- @MainThread
- private ListenableFuture<Void> submitCameraRequest(
- @NonNull CameraRequest cameraRequest) {
- checkMainThread();
- mImageCaptureControl.lockFlashMode();
- ListenableFuture<Void> captureRequestFuture =
- mImageCaptureControl.submitStillCaptureRequests(cameraRequest.getCaptureConfigs());
- Futures.addCallback(captureRequestFuture, new FutureCallback<Void>() {
- @Override
- public void onSuccess(@Nullable Void result) {
- mImageCaptureControl.unlockFlashMode();
- }
-
- @Override
- public void onFailure(@NonNull Throwable throwable) {
- if (cameraRequest.isAborted()) {
- // When the pipeline is recreated, the in-flight request is aborted and
- // retried. On legacy devices, the camera may return CancellationException
- // for the aborted request which causes the retried request to fail. Return
- // early if the request has been aborted.
- return;
- } else {
- int requestId = cameraRequest.getCaptureConfigs().get(0).getId();
- if (throwable instanceof ImageCaptureException) {
- mImagePipeline.notifyCaptureError(
- CaptureError.of(requestId, (ImageCaptureException) throwable));
- } else {
- mImagePipeline.notifyCaptureError(
- CaptureError.of(requestId, new ImageCaptureException(
- ERROR_CAPTURE_FAILED,
- "Failed to submit capture request",
- throwable)));
- }
- }
- mImageCaptureControl.unlockFlashMode();
- }
- }, mainThreadExecutor());
- return captureRequestFuture;
- }
-
@VisibleForTesting
- boolean hasCapturingRequest() {
- return mCapturingRequest != null;
- }
+ boolean hasCapturingRequest();
+ /**
+ * Returns the capture request being processed currently.
+ */
@VisibleForTesting
@Nullable
- public RequestWithCallback getCapturingRequest() {
- return mCapturingRequest;
- }
+ RequestWithCallback getCapturingRequest();
+ /**
+ * Returns the requests that have not received a result or an error yet.
+ */
+ @NonNull
@VisibleForTesting
- List<RequestWithCallback> getIncompleteRequests() {
- return mIncompleteRequests;
- }
+ List<RequestWithCallback> getIncompleteRequests();
+ /**
+ * Returns the {@link ImagePipeline} instance used under the hood.
+ */
@VisibleForTesting
@NonNull
- public ImagePipeline getImagePipeline() {
- return mImagePipeline;
- }
-
- @Override
- public void onImageClose(@NonNull ImageProxy image) {
- mainThreadExecutor().execute(this::issueNextRequest);
- }
+ ImagePipeline getImagePipeline();
@AutoValue
abstract static class CaptureError {
@@ -318,4 +115,18 @@
}
}
+ /**
+ * Interface for deferring creation of a {@link TakePictureManager}.
+ */
+ interface Provider {
+ /**
+ * Creates a new, initialized instance of a {@link TakePictureManager}.
+ *
+ * @param imageCaptureControl Used by TakePictureManager to control an
+ * {@link ImageCapture} instance.
+ * @return The {@code TakePictureManager} instance.
+ */
+ @NonNull
+ TakePictureManager newInstance(@NonNull ImageCaptureControl imageCaptureControl);
+ }
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManagerImpl.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManagerImpl.java
new file mode 100644
index 0000000..e356a50
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManagerImpl.java
@@ -0,0 +1,317 @@
+/*
+ * Copyright 2022 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.core.imagecapture;
+
+import static androidx.camera.core.ImageCapture.ERROR_CAMERA_CLOSED;
+import static androidx.camera.core.ImageCapture.ERROR_CAPTURE_FAILED;
+import static androidx.camera.core.impl.utils.Threads.checkMainThread;
+import static androidx.camera.core.impl.utils.executor.CameraXExecutors.directExecutor;
+import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
+import static androidx.core.util.Preconditions.checkState;
+
+import static java.util.Objects.requireNonNull;
+
+import android.util.Log;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.camera.core.ForwardingImageProxy.OnImageCloseListener;
+import androidx.camera.core.ImageCapture;
+import androidx.camera.core.ImageCaptureException;
+import androidx.camera.core.ImageProxy;
+import androidx.camera.core.Logger;
+import androidx.camera.core.impl.utils.futures.FutureCallback;
+import androidx.camera.core.impl.utils.futures.Futures;
+import androidx.core.util.Pair;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.List;
+
+/**
+ * Manages {@link ImageCapture#takePicture} calls.
+ *
+ * <p>In coming requests are added to a queue and later sent to camera one at a time. Only one
+ * in-flight request is allowed at a time. The next request cannot be sent until the current one
+ * is completed by camera. However, it allows multiple concurrent requests for post-processing,
+ * as {@link ImagePipeline} supports parallel processing.
+ *
+ * <p>This class selectively propagates callbacks from camera and {@link ImagePipeline} to the
+ * app. e.g. it may choose to retry the request before sending the {@link ImageCaptureException}
+ * to the app.
+ *
+ * <p>The thread safety is guaranteed by using the main thread.
+ */
+public class TakePictureManagerImpl implements TakePictureManager, OnImageCloseListener,
+ TakePictureRequest.RetryControl {
+
+ private static final String TAG = "TakePictureManagerImpl";
+
+ // Queue of new requests that have not been sent to the pipeline/camera.
+ @VisibleForTesting
+ final Deque<TakePictureRequest> mNewRequests = new ArrayDeque<>();
+ final ImageCaptureControl mImageCaptureControl;
+ ImagePipeline mImagePipeline;
+
+ // The current request being processed by the camera. Only one request can be processed by
+ // the camera at the same time. Null if the camera is idle.
+ @Nullable
+ private RequestWithCallback mCapturingRequest;
+ // The current requests that have not received a result or an error.
+ private final List<RequestWithCallback> mIncompleteRequests;
+
+ // Once paused, the class waits until the class is resumed to handle new requests.
+ boolean mPaused = false;
+
+ /**
+ * @param imageCaptureControl for controlling {@link ImageCapture}
+ */
+ @MainThread
+ public TakePictureManagerImpl(@NonNull ImageCaptureControl imageCaptureControl) {
+ checkMainThread();
+ mImageCaptureControl = imageCaptureControl;
+ mIncompleteRequests = new ArrayList<>();
+ }
+
+ /**
+ * Sets the {@link ImagePipeline} for building capture requests and post-processing camera
+ * output.
+ */
+ @MainThread
+ @Override
+ public void setImagePipeline(@NonNull ImagePipeline imagePipeline) {
+ checkMainThread();
+ mImagePipeline = imagePipeline;
+ mImagePipeline.setOnImageCloseListener(this);
+ }
+
+ /**
+ * Adds requests to the queue.
+ *
+ * <p>The requests in the queue will be executed based on the order being added.
+ */
+ @MainThread
+ @Override
+ public void offerRequest(@NonNull TakePictureRequest takePictureRequest) {
+ checkMainThread();
+ mNewRequests.offer(takePictureRequest);
+ issueNextRequest();
+ }
+
+ @MainThread
+ @Override
+ public void retryRequest(@NonNull TakePictureRequest request) {
+ checkMainThread();
+ Logger.d(TAG, "Add a new request for retrying.");
+ // Insert the request to the front of the queue.
+ mNewRequests.addFirst(request);
+ // Try to issue the newly added request in case condition allows.
+ issueNextRequest();
+ }
+
+ /**
+ * Pauses sending request to camera.
+ */
+ @MainThread
+ @Override
+ public void pause() {
+ checkMainThread();
+ mPaused = true;
+
+ // Always retry because the camera may not send an error callback during the reset.
+ if (mCapturingRequest != null) {
+ mCapturingRequest.abortSilentlyAndRetry();
+ }
+ }
+
+ /**
+ * Resumes sending request to camera.
+ */
+ @MainThread
+ @Override
+ public void resume() {
+ checkMainThread();
+ mPaused = false;
+ issueNextRequest();
+ }
+
+ /**
+ * Clears the requests queue.
+ */
+ @MainThread
+ @Override
+ public void abortRequests() {
+ checkMainThread();
+ ImageCaptureException exception =
+ new ImageCaptureException(ERROR_CAMERA_CLOSED, "Camera is closed.", null);
+
+ // Clear pending request first so aborting in-flight request won't trigger another capture.
+ for (TakePictureRequest request : mNewRequests) {
+ request.onError(exception);
+ }
+ mNewRequests.clear();
+
+ // Abort the in-flight request after clearing the pending requests.
+ // Snapshot to avoid concurrent modification with the removal in getCompleteFuture().
+ List<RequestWithCallback> requestsSnapshot = new ArrayList<>(mIncompleteRequests);
+ for (RequestWithCallback request : requestsSnapshot) {
+ // TODO: optimize the performance by not processing aborted requests.
+ request.abortAndSendErrorToApp(exception);
+ }
+ }
+
+ /**
+ * Issues the next request if conditions allow.
+ */
+ @MainThread
+ void issueNextRequest() {
+ checkMainThread();
+ Log.d(TAG, "Issue the next TakePictureRequest.");
+ if (hasCapturingRequest()) {
+ Log.d(TAG, "There is already a request in-flight.");
+ return;
+ }
+ if (mPaused) {
+ Log.d(TAG, "The class is paused.");
+ return;
+ }
+ if (mImagePipeline.getCapacity() == 0) {
+ Log.d(TAG, "Too many acquire images. Close image to be able to process next.");
+ return;
+ }
+ TakePictureRequest request = mNewRequests.poll();
+ if (request == null) {
+ Log.d(TAG, "No new request.");
+ return;
+ }
+
+ RequestWithCallback requestWithCallback = new RequestWithCallback(request, this);
+ trackCurrentRequests(requestWithCallback);
+
+ // Send requests.
+ Pair<CameraRequest, ProcessingRequest> requests =
+ mImagePipeline.createRequests(request, requestWithCallback,
+ requestWithCallback.getCaptureFuture());
+ CameraRequest cameraRequest = requireNonNull(requests.first);
+ ProcessingRequest processingRequest = requireNonNull(requests.second);
+ mImagePipeline.submitProcessingRequest(processingRequest);
+ ListenableFuture<Void> captureRequestFuture = submitCameraRequest(cameraRequest);
+ requestWithCallback.setCaptureRequestFuture(captureRequestFuture);
+ }
+
+ /**
+ * Waits for the request to finish before issuing the next.
+ */
+ private void trackCurrentRequests(@NonNull RequestWithCallback requestWithCallback) {
+ checkState(!hasCapturingRequest());
+ mCapturingRequest = requestWithCallback;
+
+ // Waits for the capture to finish before issuing the next.
+ mCapturingRequest.getCaptureFuture().addListener(() -> {
+ mCapturingRequest = null;
+ issueNextRequest();
+ }, directExecutor());
+
+ // Track all incomplete requests so we can abort them when UseCase is detached.
+ mIncompleteRequests.add(requestWithCallback);
+ requestWithCallback.getCompleteFuture().addListener(() -> {
+ mIncompleteRequests.remove(requestWithCallback);
+ }, directExecutor());
+ }
+
+ /**
+ * Submit a request to camera and post-processing pipeline.
+ *
+ * <p>Flash is locked/unlocked during the flight of a {@link CameraRequest}.
+ */
+ @MainThread
+ private ListenableFuture<Void> submitCameraRequest(
+ @NonNull CameraRequest cameraRequest) {
+ checkMainThread();
+ mImageCaptureControl.lockFlashMode();
+ ListenableFuture<Void> captureRequestFuture =
+ mImageCaptureControl.submitStillCaptureRequests(cameraRequest.getCaptureConfigs());
+ Futures.addCallback(captureRequestFuture, new FutureCallback<Void>() {
+ @Override
+ public void onSuccess(@Nullable Void result) {
+ mImageCaptureControl.unlockFlashMode();
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable throwable) {
+ if (cameraRequest.isAborted()) {
+ // When the pipeline is recreated, the in-flight request is aborted and
+ // retried. On legacy devices, the camera may return CancellationException
+ // for the aborted request which causes the retried request to fail. Return
+ // early if the request has been aborted.
+ return;
+ } else {
+ int requestId = cameraRequest.getCaptureConfigs().get(0).getId();
+ if (throwable instanceof ImageCaptureException) {
+ mImagePipeline.notifyCaptureError(
+ CaptureError.of(requestId, (ImageCaptureException) throwable));
+ } else {
+ mImagePipeline.notifyCaptureError(
+ CaptureError.of(requestId, new ImageCaptureException(
+ ERROR_CAPTURE_FAILED,
+ "Failed to submit capture request",
+ throwable)));
+ }
+ }
+ mImageCaptureControl.unlockFlashMode();
+ }
+ }, mainThreadExecutor());
+ return captureRequestFuture;
+ }
+
+ @VisibleForTesting
+ @Override
+ public boolean hasCapturingRequest() {
+ return mCapturingRequest != null;
+ }
+
+ @VisibleForTesting
+ @Nullable
+ @Override
+ public RequestWithCallback getCapturingRequest() {
+ return mCapturingRequest;
+ }
+
+ @NonNull
+ @VisibleForTesting
+ @Override
+ public List<RequestWithCallback> getIncompleteRequests() {
+ return mIncompleteRequests;
+ }
+
+ @VisibleForTesting
+ @NonNull
+ @Override
+ public ImagePipeline getImagePipeline() {
+ return mImagePipeline;
+ }
+
+ @Override
+ public void onImageClose(@NonNull ImageProxy image) {
+ mainThreadExecutor().execute(this::issueNextRequest);
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureRequest.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureRequest.java
index 4ef3f3b..d49c06b9 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureRequest.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureRequest.java
@@ -82,14 +82,14 @@
* Gets the app provided options for on-disk capture.
*/
@Nullable
- abstract ImageCapture.OutputFileOptions getOutputFileOptions();
+ public abstract ImageCapture.OutputFileOptions getOutputFileOptions();
/**
* A snapshot of {@link ImageCapture#getViewPortCropRect()} when
* {@link ImageCapture#takePicture} is called.
*/
@NonNull
- abstract Rect getCropRect();
+ public abstract Rect getCropRect();
/**
* A snapshot of {@link ImageCapture#getSensorToBufferTransformMatrix()} when
@@ -102,14 +102,14 @@
* A snapshot of rotation degrees when {@link ImageCapture#takePicture} is called.
*/
@ImageOutputConfig.RotationValue
- abstract int getRotationDegrees();
+ public abstract int getRotationDegrees();
/**
* A snapshot of {@link ImageCaptureConfig#getJpegQuality()} when
* {@link ImageCapture#takePicture} is called.
*/
@IntRange(from = 1, to = 100)
- abstract int getJpegQuality();
+ public abstract int getJpegQuality();
/**
* Gets the capture mode of the request.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java
index 347a25c..9d6f159 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java
@@ -21,10 +21,16 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.camera.core.ExtendableBuilder;
+import androidx.camera.core.ImageCapture;
import androidx.camera.core.UseCase;
+import androidx.camera.core.imagecapture.ImageCaptureControl;
+import androidx.camera.core.imagecapture.TakePictureManager;
+import androidx.camera.core.imagecapture.TakePictureManagerImpl;
import androidx.camera.core.impl.stabilization.StabilizationMode;
import androidx.camera.core.internal.TargetConfig;
+import java.util.Objects;
+
/**
* Configuration containing options for use cases.
*
@@ -108,6 +114,10 @@
Option<Integer> OPTION_VIDEO_STABILIZATION_MODE =
Option.create("camerax.core.useCase.videoStabilizationMode", int.class);
+ Option<TakePictureManager.Provider> OPTION_TAKE_PICTURE_MANAGER_PROVIDER =
+ Option.create("camerax.core.useCase.takePictureManagerProvider",
+ TakePictureManager.Provider.class);
+
// *********************************************************************************************
/**
@@ -329,6 +339,22 @@
}
/**
+ * @return The {@link TakePictureManager} implementation for {@link ImageCapture} use case.
+ */
+ @NonNull
+ default TakePictureManager.Provider getTakePictureManagerProvider() {
+ return Objects.requireNonNull(retrieveOption(OPTION_TAKE_PICTURE_MANAGER_PROVIDER,
+ new TakePictureManager.Provider() {
+ @NonNull
+ @Override
+ public TakePictureManager newInstance(
+ @NonNull ImageCaptureControl imageCaptureControl) {
+ return new TakePictureManagerImpl(imageCaptureControl);
+ }
+ }));
+ }
+
+ /**
* Builder for a {@link UseCase}.
*
* @param <T> The type of the object which will be built by {@link #build()}.
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureExtTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureExtTest.kt
index be859db..2837e7d 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureExtTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureExtTest.kt
@@ -27,16 +27,19 @@
import androidx.camera.testing.impl.fakes.FakeImageProxy
import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
import androidx.test.core.app.ApplicationProvider
+import androidx.testutils.assertThrows
import com.google.common.truth.Truth.assertThat
-import java.io.File
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.async
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
+import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows
@@ -47,9 +50,14 @@
@DoNotInstrument
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
class ImageCaptureExtTest {
+ @get:Rule
+ val temporaryFolder =
+ TemporaryFolder(ApplicationProvider.getApplicationContext<Context>().cacheDir)
+
private val context = ApplicationProvider.getApplicationContext<Context>()
- private val fakeOutputFileOptions =
- ImageCapture.OutputFileOptions.Builder(File("fake_path")).build()
+ private val fakeOutputFileOptions by lazy {
+ ImageCapture.OutputFileOptions.Builder(temporaryFolder.newFile("fake_path")).build()
+ }
private lateinit var cameraProvider: ProcessCameraProvider
private lateinit var imageCapture: ImageCapture
@@ -88,12 +96,11 @@
fun takePicture_inMemory_canGetImage(): Unit = runTest {
// Arrange
val imageProxy = FakeImageProxy(FakeImageInfo())
+ val fakeTakePictureManager = FakeAppConfig.getTakePictureManager()!!
+ fakeTakePictureManager.enqueueImageProxy(imageProxy)
// Arrange & Act.
val takePictureAsync = MainScope().async { imageCapture.takePicture() }
- Shadows.shadowOf(Looper.getMainLooper()).idle()
- val imageCaptureCallback = imageCapture.getTakePictureRequest()?.inMemoryCallback
- imageCaptureCallback?.onCaptureSuccess(imageProxy)
// Assert.
Shadows.shadowOf(Looper.getMainLooper()).idle()
@@ -101,6 +108,22 @@
}
@Test
+ fun takePicture_inMemory_imageProxyIsNotDeliveredClosed(): Unit = runTest {
+ // Arrange
+ val imageProxy = FakeImageProxy(FakeImageInfo())
+ val fakeTakePictureManager = FakeAppConfig.getTakePictureManager()!!
+ fakeTakePictureManager.enqueueImageProxy(imageProxy)
+
+ // Arrange & Act.
+ val takePictureAsync = MainScope().async { imageCapture.takePicture() }
+
+ // Assert.
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ assertThat(takePictureAsync.await()).isEqualTo(imageProxy)
+ assertThat(imageProxy.isClosed).isFalse()
+ }
+
+ @Test
fun takePicture_inMemory_canCancel(): Unit = runTest {
// Arrange & Act.
val takePictureAsync = MainScope().async { imageCapture.takePicture() }
@@ -110,6 +133,25 @@
}
@Test
+ fun takePicture_inMemory_cancelClosesUndeliveredImage(): Unit = runTest {
+ // Arrange
+ val imageProxy = FakeImageProxy(FakeImageInfo())
+ FakeAppConfig.getTakePictureManager()!!.disableAutoComplete = true
+
+ // Arrange & Act.
+ val takePictureAsync = MainScope().async { imageCapture.takePicture() }
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ val imageCaptureCallback = imageCapture.getTakePictureRequest()?.inMemoryCallback
+ takePictureAsync.cancel()
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ imageCaptureCallback?.onCaptureSuccess(imageProxy)
+
+ // Assert.
+ assertThrows<CancellationException> { takePictureAsync.await() }
+ assertThat(imageProxy.isClosed).isTrue()
+ }
+
+ @Test
fun takePicture_inMemory_canPropagateCaptureStarted(): Unit = runTest {
// Arrange.
var callbackCalled = false
@@ -135,6 +177,7 @@
var callbackCalled = false
val progress = 100
var resultProgress = 0
+ FakeAppConfig.getTakePictureManager()!!.disableAutoComplete = true
// Act.
val takePictureAsync =
@@ -163,6 +206,7 @@
var callbackCalled = false
val bitmap = Bitmap.createBitmap(800, 600, Bitmap.Config.ARGB_8888)
lateinit var resultBitmap: Bitmap
+ FakeAppConfig.getTakePictureManager()!!.disableAutoComplete = true
// Act.
val takePictureAsync =
@@ -189,15 +233,14 @@
fun takePicture_onDisk_canGetResult(): Unit = runTest {
// Arrange
val outputFileResults = ImageCapture.OutputFileResults(null)
+ val fakeTakePictureManager = FakeAppConfig.getTakePictureManager()!!
+ fakeTakePictureManager.enqueueOutputFileResults(outputFileResults)
// Arrange & Act.
val takePictureAsync =
MainScope().async {
imageCapture.takePicture(outputFileOptions = fakeOutputFileOptions)
}
- Shadows.shadowOf(Looper.getMainLooper()).idle()
- val imageCaptureCallback = imageCapture.getTakePictureRequest()?.onDiskCallback
- imageCaptureCallback?.onImageSaved(outputFileResults)
// Assert.
Shadows.shadowOf(Looper.getMainLooper()).idle()
@@ -245,6 +288,7 @@
var callbackCalled = false
val progress = 100
var resultProgress = 0
+ FakeAppConfig.getTakePictureManager()!!.disableAutoComplete = true
// Act.
val takePictureAsync =
@@ -274,6 +318,7 @@
var callbackCalled = false
val bitmap = Bitmap.createBitmap(800, 600, Bitmap.Config.ARGB_8888)
lateinit var resultBitmap: Bitmap
+ FakeAppConfig.getTakePictureManager()!!.disableAutoComplete = true
// Act.
val takePictureAsync =
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeTakePictureRequest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeTakePictureRequest.kt
index 7b775e8..87e05b0 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeTakePictureRequest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeTakePictureRequest.kt
@@ -112,7 +112,7 @@
return fileOptions
}
- internal override fun getCropRect(): Rect {
+ override fun getCropRect(): Rect {
return Rect(0, 0, 640, 480)
}
@@ -120,11 +120,11 @@
return Matrix()
}
- internal override fun getRotationDegrees(): Int {
+ override fun getRotationDegrees(): Int {
return ROTATION_DEGREES
}
- internal override fun getJpegQuality(): Int {
+ override fun getJpegQuality(): Int {
return JPEG_QUALITY
}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt
index db131ac..4f02ad7 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt
@@ -50,7 +50,7 @@
private val imagePipeline = FakeImagePipeline()
private val imageCaptureControl = FakeImageCaptureControl()
private val takePictureManager =
- TakePictureManager(imageCaptureControl).also { it.imagePipeline = imagePipeline }
+ TakePictureManagerImpl(imageCaptureControl).also { it.imagePipeline = imagePipeline }
private val exception = ImageCaptureException(ImageCapture.ERROR_UNKNOWN, "", null)
@After
diff --git a/camera/camera-extensions/build.gradle b/camera/camera-extensions/build.gradle
index 034920a..c7e4120 100644
--- a/camera/camera-extensions/build.gradle
+++ b/camera/camera-extensions/build.gradle
@@ -53,10 +53,10 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has its own MockMaker
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.kotlinCoroutinesAndroid)
androidTestImplementation(libs.kotlinStdlib)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has its own MockMaker
+ androidTestImplementation(libs.mockitoCore)
androidTestImplementation(libs.testUiautomator)
androidTestImplementation(libs.truth)
androidTestImplementation(project(":camera:camera-camera2"))
diff --git a/camera/camera-media3-effect/api/current.txt b/camera/camera-media3-effect/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/camera/camera-media3-effect/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/camera/camera-media3-effect/api/res-current.txt b/camera/camera-media3-effect/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/camera/camera-media3-effect/api/res-current.txt
diff --git a/camera/camera-media3-effect/api/restricted_current.txt b/camera/camera-media3-effect/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/camera/camera-media3-effect/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/camera/camera-media3-effect/build.gradle b/camera/camera-media3-effect/build.gradle
new file mode 100644
index 0000000..4383949
--- /dev/null
+++ b/camera/camera-media3-effect/build.gradle
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("kotlin-android")
+}
+
+dependencies {
+ api(project(":camera:camera-core"))
+ implementation(libs.media3Common)
+ implementation(libs.media3Effect)
+
+ testImplementation(libs.kotlinCoroutinesAndroid)
+ testImplementation(libs.kotlinCoroutinesTest)
+ testImplementation(libs.kotlinStdlib)
+ testImplementation(libs.testCore)
+ testImplementation(libs.testRunner)
+ testImplementation(libs.junit)
+ testImplementation(libs.truth)
+ testImplementation(libs.robolectric)
+
+ androidTestImplementation(libs.testExtJunit)
+ androidTestImplementation(libs.testCore)
+ androidTestImplementation(libs.testRunner)
+ androidTestImplementation(libs.testRules)
+ androidTestImplementation(libs.truth)
+ androidTestImplementation(libs.kotlinStdlib)
+ androidTestImplementation(libs.kotlinCoroutinesAndroid)
+}
+android {
+ testOptions.unitTests.includeAndroidResources = true
+ namespace "androidx.camera.media3.effect"
+}
+androidx {
+ name = "Camera Media3 Effect"
+ type = LibraryType.PUBLISHED_LIBRARY
+ inceptionYear = "2024"
+ description = "Media3 effect components for the Jetpack Camera Library, a library providing a" +
+ " seamless integration that enables media3 effect in CameraX."
+}
\ No newline at end of file
diff --git a/camera/camera-media3-effect/src/androidTest/java/androidx/camera/media3/effect/Media3EffectDeviceTest.kt b/camera/camera-media3-effect/src/androidTest/java/androidx/camera/media3/effect/Media3EffectDeviceTest.kt
new file mode 100644
index 0000000..4840bb7
--- /dev/null
+++ b/camera/camera-media3-effect/src/androidTest/java/androidx/camera/media3/effect/Media3EffectDeviceTest.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.media3.effect
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Instrumented tests for [Media3Effect]. */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 21)
+class Media3EffectDeviceTest {
+
+ @Test
+ fun smokeTest() {
+ assertThat(true).isTrue()
+ }
+}
diff --git a/camera/camera-media3-effect/src/main/java/androidx/camera/media3/effect/Media3Effect.kt b/camera/camera-media3-effect/src/main/java/androidx/camera/media3/effect/Media3Effect.kt
new file mode 100644
index 0000000..7ecd11a
--- /dev/null
+++ b/camera/camera-media3-effect/src/main/java/androidx/camera/media3/effect/Media3Effect.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.media3.effect
+
+import androidx.annotation.RestrictTo
+import androidx.camera.core.CameraEffect
+import androidx.camera.core.SurfaceProcessor
+import androidx.core.util.Consumer
+import java.util.concurrent.Executor
+
+/** A CameraEffect that inserts media3 effect into CameraX pipeline */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class Media3Effect(
+ targets: Int,
+ executor: Executor,
+ surfaceProcessor: SurfaceProcessor,
+ errorListener: Consumer<Throwable>
+) : CameraEffect(targets, executor, surfaceProcessor, errorListener) {}
diff --git a/camera/camera-media3-effect/src/test/java/androidx/camera/media3/effect/Media3EffectTest.kt b/camera/camera-media3-effect/src/test/java/androidx/camera/media3/effect/Media3EffectTest.kt
new file mode 100644
index 0000000..0f3365e
--- /dev/null
+++ b/camera/camera-media3-effect/src/test/java/androidx/camera/media3/effect/Media3EffectTest.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.media3.effect
+
+import android.os.Build
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+/** Unit tests for [Media3Effect]. */
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class Media3EffectTest {
+
+ // TODO: replace this with a real test.
+ @Test
+ fun smokeTest() {
+ assertThat(true).isTrue()
+ }
+}
diff --git a/camera/camera-testing/api/current.txt b/camera/camera-testing/api/current.txt
index fbf779a..161b797 100644
--- a/camera/camera-testing/api/current.txt
+++ b/camera/camera-testing/api/current.txt
@@ -42,10 +42,12 @@
ctor public FakeCameraControl(androidx.camera.core.impl.CameraControlInternal.ControlUpdateCallback);
ctor public FakeCameraControl(java.util.concurrent.Executor, androidx.camera.core.impl.CameraControlInternal.ControlUpdateCallback);
method public void addInteropConfig(androidx.camera.core.impl.Config);
+ method public void addOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+ method public void addOnNewCaptureRequestListener(java.util.concurrent.Executor, androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
method public void addZslConfig(androidx.camera.core.impl.SessionConfig.Builder);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelFocusAndMetering();
method public void clearInteropConfig();
- method public void clearNewCaptureRequestListener();
+ method @Deprecated public void clearNewCaptureRequestListener();
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enableTorch(boolean);
method public int getExposureCompensationIndex();
method public int getFlashMode();
@@ -62,11 +64,14 @@
method public void notifyAllRequestsOnCaptureCancelled();
method public void notifyAllRequestsOnCaptureCompleted(androidx.camera.core.impl.CameraCaptureResult);
method public void notifyAllRequestsOnCaptureFailed();
+ method public void notifyLastRequestOnCaptureCompleted(androidx.camera.core.impl.CameraCaptureResult);
+ method public void removeOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+ method public void removeOnNewCaptureRequestListeners(java.util.List<androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener!>);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer!> setExposureCompensationIndex(int);
method public void setFlashMode(int);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setLinearZoom(float);
- method public void setOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
- method public void setOnNewCaptureRequestListener(java.util.concurrent.Executor, androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+ method @Deprecated public void setOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+ method @Deprecated public void setOnNewCaptureRequestListener(java.util.concurrent.Executor, androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
method public void setScreenFlash(androidx.camera.core.ImageCapture.ScreenFlash?);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setZoomRatio(float);
method public void setZslDisabledByUserCaseConfig(boolean);
diff --git a/camera/camera-testing/api/restricted_current.txt b/camera/camera-testing/api/restricted_current.txt
index fbf779a..161b797 100644
--- a/camera/camera-testing/api/restricted_current.txt
+++ b/camera/camera-testing/api/restricted_current.txt
@@ -42,10 +42,12 @@
ctor public FakeCameraControl(androidx.camera.core.impl.CameraControlInternal.ControlUpdateCallback);
ctor public FakeCameraControl(java.util.concurrent.Executor, androidx.camera.core.impl.CameraControlInternal.ControlUpdateCallback);
method public void addInteropConfig(androidx.camera.core.impl.Config);
+ method public void addOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+ method public void addOnNewCaptureRequestListener(java.util.concurrent.Executor, androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
method public void addZslConfig(androidx.camera.core.impl.SessionConfig.Builder);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelFocusAndMetering();
method public void clearInteropConfig();
- method public void clearNewCaptureRequestListener();
+ method @Deprecated public void clearNewCaptureRequestListener();
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enableTorch(boolean);
method public int getExposureCompensationIndex();
method public int getFlashMode();
@@ -62,11 +64,14 @@
method public void notifyAllRequestsOnCaptureCancelled();
method public void notifyAllRequestsOnCaptureCompleted(androidx.camera.core.impl.CameraCaptureResult);
method public void notifyAllRequestsOnCaptureFailed();
+ method public void notifyLastRequestOnCaptureCompleted(androidx.camera.core.impl.CameraCaptureResult);
+ method public void removeOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+ method public void removeOnNewCaptureRequestListeners(java.util.List<androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener!>);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer!> setExposureCompensationIndex(int);
method public void setFlashMode(int);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setLinearZoom(float);
- method public void setOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
- method public void setOnNewCaptureRequestListener(java.util.concurrent.Executor, androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+ method @Deprecated public void setOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+ method @Deprecated public void setOnNewCaptureRequestListener(java.util.concurrent.Executor, androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
method public void setScreenFlash(androidx.camera.core.ImageCapture.ScreenFlash?);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setZoomRatio(float);
method public void setZslDisabledByUserCaseConfig(boolean);
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java
index 41958c4..c7f41a5 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java
@@ -28,6 +28,10 @@
import androidx.camera.testing.impl.fakes.FakeCameraDeviceSurfaceManager;
import androidx.camera.testing.impl.fakes.FakeCameraFactory;
import androidx.camera.testing.impl.fakes.FakeUseCaseConfigFactory;
+import androidx.camera.testing.impl.wrappers.TakePictureManagerWrapper;
+
+import java.util.ArrayList;
+import java.util.List;
/**
* Convenience class for generating a fake {@link CameraXConfig}.
@@ -47,6 +51,9 @@
@Nullable
private static FakeCamera sFrontCamera = null;
+ @Nullable
+ private static FakeUseCaseConfigFactory sFakeUseCaseConfigFactory;
+
/** Generates a fake {@link CameraXConfig}. */
@NonNull
public static CameraXConfig create() {
@@ -59,28 +66,26 @@
*/
@NonNull
public static CameraXConfig create(@Nullable CameraSelector availableCamerasSelector) {
+ FakeCameraFactory cameraFactory = createCameraFactory(availableCamerasSelector);
+
final CameraFactory.Provider cameraFactoryProvider =
- (ignored1, ignored2, ignored3, ignore4) -> {
- final FakeCameraFactory cameraFactory = new FakeCameraFactory(
- availableCamerasSelector);
- cameraFactory.insertCamera(CameraSelector.LENS_FACING_BACK,
- DEFAULT_BACK_CAMERA_ID,
- FakeAppConfig::getBackCamera);
- cameraFactory.insertCamera(CameraSelector.LENS_FACING_FRONT,
- DEFAULT_FRONT_CAMERA_ID,
- FakeAppConfig::getFrontCamera);
- final CameraCoordinator cameraCoordinator = new FakeCameraCoordinator();
- cameraFactory.setCameraCoordinator(cameraCoordinator);
- return cameraFactory;
- };
+ (ignored1, ignored2, ignored3, ignore4) -> cameraFactory;
final CameraDeviceSurfaceManager.Provider surfaceManagerProvider =
(ignored1, ignored2, ignored3) -> new FakeCameraDeviceSurfaceManager();
+ List<FakeCamera> fakeCameras = new ArrayList<>();
+ for (String cameraId : cameraFactory.getAvailableCameraIds()) {
+ fakeCameras.add((FakeCamera) cameraFactory.getCamera(cameraId));
+ }
+
+ sFakeUseCaseConfigFactory = new FakeUseCaseConfigFactory(fakeCameras);
+
final CameraXConfig.Builder appConfigBuilder = new CameraXConfig.Builder()
.setCameraFactoryProvider(cameraFactoryProvider)
.setDeviceSurfaceManagerProvider(surfaceManagerProvider)
- .setUseCaseConfigFactoryProvider(ignored -> new FakeUseCaseConfigFactory());
+ .setUseCaseConfigFactoryProvider(
+ ignored -> sFakeUseCaseConfigFactory);
if (availableCamerasSelector != null) {
appConfigBuilder.setAvailableCamerasLimiter(availableCamerasSelector);
@@ -89,6 +94,21 @@
return appConfigBuilder.build();
}
+ private static FakeCameraFactory createCameraFactory(
+ @Nullable CameraSelector availableCamerasSelector) {
+ FakeCameraFactory cameraFactory = new FakeCameraFactory(availableCamerasSelector);
+ cameraFactory.insertCamera(
+ CameraSelector.LENS_FACING_BACK,
+ DEFAULT_BACK_CAMERA_ID,
+ FakeAppConfig::getBackCamera);
+ cameraFactory.insertCamera(CameraSelector.LENS_FACING_FRONT,
+ DEFAULT_FRONT_CAMERA_ID,
+ FakeAppConfig::getFrontCamera);
+ final CameraCoordinator cameraCoordinator = new FakeCameraCoordinator();
+ cameraFactory.setCameraCoordinator(cameraCoordinator);
+ return cameraFactory;
+ }
+
/**
* Returns the default fake back camera that is used internally by CameraX.
*/
@@ -126,4 +146,20 @@
return create();
}
}
+
+ /**
+ * Returns the {@link TakePictureManagerWrapper} being used for image capture.
+ *
+ * <p> Note that this may be null if {@link androidx.camera.core.ImageCapture} is still not set
+ * up and bound to a camera.
+ */
+ @Nullable
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static TakePictureManagerWrapper getTakePictureManager() {
+ if (sFakeUseCaseConfigFactory == null) {
+ return null;
+ }
+ return sFakeUseCaseConfigFactory.getTakePictureManager();
+ }
+
}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java
index a3b256b..b4d661b 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java
@@ -21,6 +21,7 @@
import android.graphics.Rect;
+import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.camera.core.FocusMeteringAction;
@@ -46,6 +47,8 @@
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
@@ -76,13 +79,18 @@
* <p> {@link CameraXExecutors#directExecutor} via default, unless some other executor is set
* via {@link #FakeCameraControl(Executor, CameraControlInternal.ControlUpdateCallback)}.
*/
- @NonNull private final Executor mExecutor;
+ @NonNull
+ private final Executor mExecutor;
private final ControlUpdateCallback mControlUpdateCallback;
private final SessionConfig.Builder mSessionConfigBuilder = new SessionConfig.Builder();
@ImageCapture.FlashMode
private int mFlashMode = FLASH_MODE_OFF;
private final ArrayList<CaptureConfig> mSubmittedCaptureRequests = new ArrayList<>();
+ @Deprecated
private Pair<Executor, OnNewCaptureRequestListener> mOnNewCaptureRequestListener;
+ @GuardedBy("mOnNewCaptureRequestListeners")
+ private final List<Pair<Executor, OnNewCaptureRequestListener>> mOnNewCaptureRequestListeners =
+ new ArrayList<>();
private MutableOptionsBundle mInteropConfig = MutableOptionsBundle.create();
private final ArrayList<CallbackToFutureAdapter.Completer<Void>> mSubmittedCompleterList =
new ArrayList<>();
@@ -127,7 +135,8 @@
* Constructs an instance of {@link FakeCameraControl} with the
* provided {@link ControlUpdateCallback}.
*
- * @param executor {@link Executor} used to invoke the {@code controlUpdateCallback}.
+ * @param executor {@link Executor} used to invoke the {@code
+ * controlUpdateCallback}.
* @param controlUpdateCallback {@link ControlUpdateCallback} to notify events.
*/
public FakeCameraControl(@NonNull Executor executor,
@@ -180,6 +189,38 @@
}
/**
+ * Notifies last submitted request using {@link CameraCaptureCallback#onCaptureCompleted},
+ * which is invoked in the thread denoted by {@link #mExecutor}.
+ *
+ * @param result The {@link CameraCaptureResult} which is notified to all the callbacks.
+ */
+ public void notifyLastRequestOnCaptureCompleted(@NonNull CameraCaptureResult result) {
+ if (mSubmittedCaptureRequests.isEmpty() || mSubmittedCompleterList.isEmpty()) {
+ Logger.e(TAG,
+ "notifyLastRequestOnCaptureCompleted: returning early since either "
+ + "mSubmittedCaptureRequests or mSubmittedCompleterList is empty, "
+ + "mSubmittedCaptureRequests = "
+ + mSubmittedCaptureRequests + ", mSubmittedCompleterList"
+ + mSubmittedCompleterList);
+ return;
+ }
+
+ CaptureConfig captureConfig = mSubmittedCaptureRequests.get(
+ mSubmittedCaptureRequests.size() - 1);
+ for (CameraCaptureCallback cameraCaptureCallback :
+ captureConfig.getCameraCaptureCallbacks()) {
+ mExecutor.execute(() -> cameraCaptureCallback.onCaptureCompleted(
+ captureConfig.getId(), result));
+ }
+ mSubmittedCaptureRequests.remove(captureConfig);
+
+ CallbackToFutureAdapter.Completer<Void> completer = mSubmittedCompleterList.get(
+ mSubmittedCompleterList.size() - 1);
+ completer.set(null);
+ mSubmittedCompleterList.remove(completer);
+ }
+
+ /**
* Notifies all submitted requests using {@link CameraCaptureCallback#onCaptureCompleted},
* which is invoked in the thread denoted by {@link #mExecutor}.
*
@@ -288,6 +329,7 @@
public ListenableFuture<List<Void>> submitStillCaptureRequests(
@NonNull List<CaptureConfig> captureConfigs,
int captureMode, int flashType) {
+ Logger.d(TAG, "submitStillCaptureRequests: captureConfigs = " + captureConfigs);
mSubmittedCaptureRequests.addAll(captureConfigs);
mExecutor.execute(
() -> mControlUpdateCallback.onCameraControlCaptureRequests(captureConfigs));
@@ -299,12 +341,16 @@
}));
}
- if (mOnNewCaptureRequestListener != null) {
- Executor executor = Objects.requireNonNull(mOnNewCaptureRequestListener.first);
- OnNewCaptureRequestListener listener =
- Objects.requireNonNull(mOnNewCaptureRequestListener.second);
+ synchronized (mOnNewCaptureRequestListeners) {
+ Logger.d(TAG, "submitStillCaptureRequests: mOnNewCaptureRequestListeners = "
+ + mOnNewCaptureRequestListeners);
- executor.execute(() -> listener.onNewCaptureRequests(captureConfigs));
+ for (Pair<Executor, FakeCameraControl.OnNewCaptureRequestListener> listenerPair :
+ mOnNewCaptureRequestListeners) {
+ Executor executor = Objects.requireNonNull(listenerPair.first);
+ OnNewCaptureRequestListener listener = Objects.requireNonNull(listenerPair.second);
+ executor.execute(() -> listener.onNewCaptureRequests(captureConfigs));
+ }
}
return Futures.allAsList(fakeFutures);
}
@@ -348,6 +394,58 @@
}
/**
+ * Adds a listener to be notified when there are new capture requests submitted.
+ *
+ * <p> Note that the listener will be executed on the calling thread directly using
+ * {@link CameraXExecutors#directExecutor}. To specify the execution thread, use
+ * {@link #setOnNewCaptureRequestListener(Executor, OnNewCaptureRequestListener)}.
+ *
+ * @param listener {@link OnNewCaptureRequestListener} that is notified with the submitted
+ * {@link CaptureConfig} parameters when new capture requests are submitted.
+ */
+ public void addOnNewCaptureRequestListener(@NonNull OnNewCaptureRequestListener listener) {
+ addOnNewCaptureRequestListener(CameraXExecutors.directExecutor(), listener);
+ }
+
+ /**
+ * Adds a listener to be notified when there are new capture requests submitted.
+ *
+ * @param executor {@link Executor} used to notify the {@code listener}.
+ * @param listener {@link OnNewCaptureRequestListener} that is notified with the submitted
+ * {@link CaptureConfig} parameters when new capture requests are submitted.
+ */
+ public void addOnNewCaptureRequestListener(@NonNull Executor executor,
+ @NonNull OnNewCaptureRequestListener listener) {
+ synchronized (mOnNewCaptureRequestListeners) {
+ mOnNewCaptureRequestListeners.add(new Pair<>(executor, listener));
+ }
+ }
+
+ /**
+ * Removes a listener set via {@link #addOnNewCaptureRequestListener}.
+ */
+ public void removeOnNewCaptureRequestListener(@NonNull OnNewCaptureRequestListener listener) {
+ removeOnNewCaptureRequestListeners(Collections.singletonList(listener));
+ }
+
+ /**
+ * Removes a listener set via {@link #addOnNewCaptureRequestListener}.
+ */
+ public void removeOnNewCaptureRequestListeners(
+ @NonNull List<OnNewCaptureRequestListener> listeners) {
+ synchronized (mOnNewCaptureRequestListeners) {
+ Iterator<Pair<Executor, OnNewCaptureRequestListener>> iterator =
+ mOnNewCaptureRequestListeners.iterator();
+ while (iterator.hasNext()) {
+ Pair<Executor, OnNewCaptureRequestListener> element = iterator.next();
+ if (listeners.contains(element.second)) {
+ iterator.remove();
+ }
+ }
+ }
+ }
+
+ /**
* Sets a listener to be notified when there are new capture requests submitted.
*
* <p> Note that the listener will be executed on the calling thread directly using
@@ -355,8 +453,10 @@
* {@link #setOnNewCaptureRequestListener(Executor, OnNewCaptureRequestListener)}.
*
* @param listener {@link OnNewCaptureRequestListener} that is notified with the submitted
- * {@link CaptureConfig} parameters when new capture requests are submitted.
+ * {@link CaptureConfig} parameters when new capture requests are submitted.
+ * @deprecated Use {@link #addOnNewCaptureRequestListener(OnNewCaptureRequestListener)} instead.
*/
+ @Deprecated // TODO: b/359458110 - Remove all usages
public void setOnNewCaptureRequestListener(@NonNull OnNewCaptureRequestListener listener) {
setOnNewCaptureRequestListener(CameraXExecutors.directExecutor(), listener);
}
@@ -366,17 +466,31 @@
*
* @param executor {@link Executor} used to notify the {@code listener}.
* @param listener {@link OnNewCaptureRequestListener} that is notified with the submitted
- * {@link CaptureConfig} parameters when new capture requests are submitted.
+ * {@link CaptureConfig} parameters when new capture requests are submitted.
+ * @deprecated Use
+ * {@link #addOnNewCaptureRequestListener(Executor, OnNewCaptureRequestListener)}
+ * instead.
*/
+ @Deprecated // TODO: b/359458110 - Remove all usages
public void setOnNewCaptureRequestListener(@NonNull Executor executor,
@NonNull OnNewCaptureRequestListener listener) {
mOnNewCaptureRequestListener = new Pair<>(executor, listener);
+ addOnNewCaptureRequestListener(executor, listener);
}
/**
* Clears any listener set via {@link #setOnNewCaptureRequestListener}.
+ *
+ * @deprecated Use {@link #removeOnNewCaptureRequestListener(OnNewCaptureRequestListener)}
+ * instead.
*/
+ @Deprecated // TODO: b/359458110 - Remove all usages
public void clearNewCaptureRequestListener() {
+ if (mOnNewCaptureRequestListener == null) {
+ return;
+ }
+ removeOnNewCaptureRequestListener(
+ Objects.requireNonNull(mOnNewCaptureRequestListener.second));
mOnNewCaptureRequestListener = null;
}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/CaptureSimulation.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/CaptureSimulation.kt
index e960662..015bff9 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/CaptureSimulation.kt
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/CaptureSimulation.kt
@@ -16,8 +16,11 @@
package androidx.camera.testing.impl
+import android.graphics.Bitmap
import android.graphics.Rect
+import android.util.Size
import android.view.Surface
+import androidx.camera.core.Logger
import androidx.camera.core.impl.DeferrableSurface
import androidx.camera.core.impl.utils.executor.CameraXExecutors
import androidx.camera.core.impl.utils.futures.FutureCallback
@@ -36,7 +39,7 @@
private const val TAG = "CaptureSimulation"
/** Simulates a capture frame being drawn on all of the provided surfaces. */
-public suspend fun List<DeferrableSurface>.simulateCaptureFrame(): Unit = forEach {
+internal suspend fun List<DeferrableSurface>.simulateCaptureFrame(): Unit = forEach {
it.simulateCaptureFrame()
}
@@ -45,7 +48,7 @@
*
* @throws IllegalStateException If [DeferrableSurface.getSurface] provides a null surface.
*/
-public suspend fun DeferrableSurface.simulateCaptureFrame() {
+internal suspend fun DeferrableSurface.simulateCaptureFrame() {
val deferred = CompletableDeferred<Unit>()
Futures.addCallback(
@@ -53,6 +56,7 @@
object : FutureCallback<Surface?> {
override fun onSuccess(surface: Surface?) {
if (surface == null) {
+ Logger.w(TAG, "simulateCaptureFrame: surface obtained from $this is null!")
deferred.completeExceptionally(
IllegalStateException(
"Null surface obtained from ${this@simulateCaptureFrame}"
@@ -60,10 +64,9 @@
)
return
}
- val canvas =
- surface.lockCanvas(Rect(0, 0, prescribedSize.width, prescribedSize.height))
// TODO: Draw something on the canvas (e.g. fake image bitmap or alternating color).
- surface.unlockCanvasAndPost(canvas)
+ surface.simulateCaptureFrame(prescribedSize)
+
deferred.complete(Unit)
}
@@ -77,6 +80,20 @@
deferred.await()
}
+/**
+ * Simulates a capture frame being drawn on a [Surface].
+ *
+ * @param canvasSize The canvas size for drawing.
+ * @param bitmap A bitmap to draw as the capture frame, if not null.
+ */
+internal fun Surface.simulateCaptureFrame(canvasSize: Size, bitmap: Bitmap? = null) {
+ val canvas = lockCanvas(Rect(0, 0, canvasSize.width, canvasSize.height))
+ if (bitmap != null) {
+ canvas.drawBitmap(bitmap, null, Rect(0, 0, canvasSize.width, canvasSize.height), null)
+ }
+ unlockCanvasAndPost(canvas)
+}
+
// The following methods are adapters for Java invocations.
/**
@@ -88,7 +105,7 @@
* @return A [ListenableFuture] representing when the operation has been completed.
*/
@JvmOverloads
-public fun List<DeferrableSurface>.simulateCaptureFrameAsync(
+internal fun List<DeferrableSurface>.simulateCaptureFrameAsync(
executor: Executor = Dispatchers.Default.asExecutor()
): ListenableFuture<Void> {
val scope = CoroutineScope(SupervisorJob() + executor.asCoroutineDispatcher())
@@ -104,7 +121,7 @@
* @return A [ListenableFuture] representing when the operation has been completed.
*/
@JvmOverloads
-public fun DeferrableSurface.simulateCaptureFrameAsync(
+internal fun DeferrableSurface.simulateCaptureFrameAsync(
executor: Executor = Dispatchers.Default.asExecutor()
): ListenableFuture<Void> {
val scope = CoroutineScope(SupervisorJob() + executor.asCoroutineDispatcher())
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeImageProxy.java b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeImageProxy.java
index 90c4bce..5acfa2bb 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeImageProxy.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeImageProxy.java
@@ -16,6 +16,7 @@
package androidx.camera.testing.impl.fakes;
+import android.graphics.Bitmap;
import android.graphics.Rect;
import android.media.Image;
@@ -47,6 +48,8 @@
@NonNull
private ImageInfo mImageInfo;
private Image mImage;
+ @Nullable
+ private Bitmap mBitmap;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
final Object mReleaseLock = new Object();
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@@ -60,6 +63,11 @@
mImageInfo = imageInfo;
}
+ public FakeImageProxy(@NonNull ImageInfo imageInfo, @NonNull Bitmap bitmap) {
+ mImageInfo = imageInfo;
+ mBitmap = bitmap;
+ }
+
@Override
public void close() {
synchronized (mReleaseLock) {
@@ -196,4 +204,13 @@
return mReleaseFuture;
}
}
+
+ @NonNull
+ @Override
+ public Bitmap toBitmap() {
+ if (mBitmap != null) {
+ return mBitmap;
+ }
+ return ImageProxy.super.toBitmap();
+ }
}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeUseCaseConfigFactory.java b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeUseCaseConfigFactory.java
index f364c4e..9ccd9bd 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeUseCaseConfigFactory.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeUseCaseConfigFactory.java
@@ -19,6 +19,7 @@
import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_SESSION_CONFIG;
import static androidx.camera.core.impl.UseCaseConfig.OPTION_SESSION_CONFIG_UNPACKER;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_TAKE_PICTURE_MANAGER_PROVIDER;
import android.annotation.SuppressLint;
import android.hardware.camera2.CameraDevice;
@@ -30,21 +31,47 @@
import androidx.camera.core.ExperimentalZeroShutterLag;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCapture.CaptureMode;
+import androidx.camera.core.imagecapture.ImageCaptureControl;
+import androidx.camera.core.imagecapture.TakePictureManager;
import androidx.camera.core.impl.Config;
import androidx.camera.core.impl.MutableOptionsBundle;
import androidx.camera.core.impl.OptionsBundle;
import androidx.camera.core.impl.SessionConfig;
import androidx.camera.core.impl.UseCaseConfigFactory;
+import androidx.camera.testing.fakes.FakeCamera;
+import androidx.camera.testing.impl.wrappers.TakePictureManagerWrapper;
+
+import java.util.ArrayList;
+import java.util.List;
/**
* A fake implementation of {@link UseCaseConfigFactory}.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public final class FakeUseCaseConfigFactory implements UseCaseConfigFactory {
-
@Nullable
private CaptureType mLastRequestedCaptureType;
+ @Nullable
+ private TakePictureManagerWrapper mTakePictureManager;
+
+ @NonNull
+ private final List<FakeCamera> mFakeCameras = new ArrayList<>();
+
+ /**
+ * Creates a {@link FakeUseCaseConfigFactory} instance.
+ */
+ public FakeUseCaseConfigFactory() {
+ }
+
+ /**
+ * Creates a {@link FakeUseCaseConfigFactory} instance with the available {@link FakeCamera}
+ * instances.
+ */
+ public FakeUseCaseConfigFactory(@NonNull List<FakeCamera> fakeCameras) {
+ mFakeCameras.addAll(fakeCameras);
+ }
+
/**
* Returns the configuration for the given capture type, or <code>null</code> if the
* configuration cannot be produced.
@@ -66,6 +93,20 @@
mutableConfig.insertOption(OPTION_SESSION_CONFIG_UNPACKER,
new FakeSessionConfigOptionUnpacker());
+ if (captureType == CaptureType.IMAGE_CAPTURE) {
+ mutableConfig.insertOption(OPTION_TAKE_PICTURE_MANAGER_PROVIDER,
+ new TakePictureManager.Provider() {
+ @NonNull
+ @Override
+ public TakePictureManager newInstance(
+ @NonNull ImageCaptureControl imageCaptureControl) {
+ mTakePictureManager = new TakePictureManagerWrapper(
+ imageCaptureControl, mFakeCameras);
+ return mTakePictureManager;
+ }
+ });
+ }
+
return OptionsBundle.from(mutableConfig);
}
@@ -97,4 +138,12 @@
return CameraDevice.TEMPLATE_PREVIEW;
}
}
+
+ /**
+ * Returns the last provided {@link TakePictureManagerWrapper} instance.
+ */
+ @Nullable
+ public TakePictureManagerWrapper getTakePictureManager() {
+ return mTakePictureManager;
+ }
}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/wrappers/TakePictureManagerWrapper.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/wrappers/TakePictureManagerWrapper.kt
new file mode 100644
index 0000000..0de7dba
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/wrappers/TakePictureManagerWrapper.kt
@@ -0,0 +1,269 @@
+/*
+ * 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.wrappers
+
+import android.graphics.Bitmap
+import android.graphics.Matrix
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.ImageCapture.OutputFileOptions
+import androidx.camera.core.ImageCapture.OutputFileResults
+import androidx.camera.core.ImageProcessingUtil
+import androidx.camera.core.ImageProxy
+import androidx.camera.core.Logger
+import androidx.camera.core.imagecapture.Bitmap2JpegBytes
+import androidx.camera.core.imagecapture.ImageCaptureControl
+import androidx.camera.core.imagecapture.ImagePipeline
+import androidx.camera.core.imagecapture.JpegBytes2Disk
+import androidx.camera.core.imagecapture.JpegBytes2Image
+import androidx.camera.core.imagecapture.RequestWithCallback
+import androidx.camera.core.imagecapture.TakePictureManager
+import androidx.camera.core.imagecapture.TakePictureManagerImpl
+import androidx.camera.core.imagecapture.TakePictureRequest
+import androidx.camera.core.processing.Packet
+import androidx.camera.testing.fakes.FakeCamera
+import androidx.camera.testing.fakes.FakeCameraControl
+import androidx.camera.testing.impl.ExifUtil
+import androidx.camera.testing.impl.TestImageUtil
+import androidx.camera.testing.impl.fakes.FakeCameraCaptureResult
+import androidx.camera.testing.impl.fakes.FakeImageInfo
+import androidx.camera.testing.impl.fakes.FakeImageProxy
+
+/**
+ * A [TakePictureManager] implementation wrapped around the real implementation
+ * [TakePictureManagerImpl].
+ *
+ * It is used for fake cameras and provides fake image capture results when required from a camera.
+ */
+public class TakePictureManagerWrapper(
+ imageCaptureControl: ImageCaptureControl,
+ private val fakeCameras: List<FakeCamera>
+) : TakePictureManager {
+ // Try to keep the fake as close to real as possible
+ private val managerDelegate = TakePictureManagerImpl(imageCaptureControl)
+
+ private val bitmap2JpegBytes = Bitmap2JpegBytes()
+ private val jpegBytes2Disk = JpegBytes2Disk()
+ private val jpegBytes2Image = JpegBytes2Image()
+
+ private val imageProxyQueue = ArrayDeque<ImageProxy>()
+ private val outputFileResultsQueue = ArrayDeque<ImageCapture.OutputFileResults>()
+
+ /** Whether to disable auto capture completion. */
+ public var disableAutoComplete: Boolean = false
+
+ override fun setImagePipeline(imagePipeline: ImagePipeline) {
+ managerDelegate.imagePipeline = imagePipeline
+ }
+
+ override fun offerRequest(takePictureRequest: TakePictureRequest) {
+ val listeners = mutableListOf<FakeCameraControl.OnNewCaptureRequestListener>()
+
+ fakeCameras.forEach { camera ->
+ if (camera.cameraControlInternal is FakeCameraControl) {
+ (camera.cameraControlInternal as FakeCameraControl).apply {
+ val listener =
+ FakeCameraControl.OnNewCaptureRequestListener {
+ if (!disableAutoComplete) {
+ completeCapturingRequest(this)
+ }
+ }
+ listeners.add(listener)
+ addOnNewCaptureRequestListener(listener)
+ }
+ } else {
+ Logger.w(
+ TAG,
+ "Ignoring ${camera.cameraControlInternal} as it's not FakeCameraControl!"
+ )
+ }
+ }
+
+ managerDelegate.offerRequest(takePictureRequest)
+
+ fakeCameras.forEach { camera ->
+ if (camera.cameraControlInternal is FakeCameraControl) {
+ (camera.cameraControlInternal as FakeCameraControl)
+ .removeOnNewCaptureRequestListeners(listeners)
+ } else {
+ Logger.w(
+ TAG,
+ "Ignoring ${camera.cameraControlInternal} as it's not FakeCameraControl!"
+ )
+ }
+ }
+ }
+
+ override fun pause() {
+ managerDelegate.pause()
+ }
+
+ override fun resume() {
+ managerDelegate.resume()
+ }
+
+ override fun abortRequests() {
+ managerDelegate.abortRequests()
+ }
+
+ @VisibleForTesting
+ override fun hasCapturingRequest(): Boolean = managerDelegate.hasCapturingRequest()
+
+ @VisibleForTesting
+ override fun getCapturingRequest(): RequestWithCallback? = managerDelegate.capturingRequest
+
+ @VisibleForTesting
+ override fun getIncompleteRequests(): List<RequestWithCallback> =
+ managerDelegate.incompleteRequests
+
+ @VisibleForTesting
+ override fun getImagePipeline(): ImagePipeline = managerDelegate.imagePipeline
+
+ @VisibleForTesting
+ public fun completeCapturingRequest(fakeCameraControl: FakeCameraControl) {
+ Log.d(
+ TAG,
+ "completeCapturingRequest: capturingRequest = ${managerDelegate.capturingRequest}"
+ )
+ managerDelegate.capturingRequest?.apply {
+ onCaptureStarted()
+
+ // This ensures the future from CameraControlInternal#submitStillCaptureRequests() is
+ // completed and not garbage collected later
+ // TODO - notify all the new requests, not only the last one
+ fakeCameraControl.notifyLastRequestOnCaptureCompleted(FakeCameraCaptureResult())
+
+ onImageCaptured()
+
+ takePictureRequest.also { req ->
+ val outputFileOptions = req.outputFileOptions // enables smartcast for null check
+ if (req.onDiskCallback != null && outputFileOptions != null) {
+ if (outputFileResultsQueue.isEmpty()) {
+ onFinalResult(createOutputFileResults(req, outputFileOptions))
+ } else {
+ onFinalResult(outputFileResultsQueue.first())
+ outputFileResultsQueue.removeFirst()
+ }
+ } else {
+ if (imageProxyQueue.isEmpty()) {
+ onFinalResult(createImageProxy(req))
+ } else {
+ onFinalResult(imageProxyQueue.first())
+ imageProxyQueue.removeFirst()
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Enqueues an [ImageProxy] to be used as result for the next image capture with
+ * [ImageCapture.OnImageCapturedCallback].
+ *
+ * Note that the provided [ImageProxy] is consumed by next image capture and is not available
+ * for following captures. If no result is available during a capture, CameraX will create a
+ * fake image by itself and provide result based on that.
+ */
+ public fun enqueueImageProxy(imageProxy: ImageProxy) {
+ imageProxyQueue.add(imageProxy)
+ }
+
+ /**
+ * Enqueues an [OutputFileResults] to be used as result for the next image capture with
+ * [ImageCapture.OnImageSavedCallback].
+ *
+ * Note that the provided [OutputFileResults] is consumed by next image capture and is not
+ * available for following captures. If no result is available during a capture, CameraX will
+ * create a fake image by itself and provide result based on that.
+ */
+ public fun enqueueOutputFileResults(outputFileResults: ImageCapture.OutputFileResults) {
+ outputFileResultsQueue.add(outputFileResults)
+ }
+
+ private fun createOutputFileResults(
+ takePictureRequest: TakePictureRequest,
+ outputFileOptions: OutputFileOptions
+ ): ImageCapture.OutputFileResults {
+ // TODO - Take a bitmap as input and use that directly
+ val bytesPacket =
+ takePictureRequest.convertBitmapToBytes(
+ TestImageUtil.createBitmap(
+ takePictureRequest.cropRect.width(),
+ takePictureRequest.cropRect.height()
+ )
+ )
+ return jpegBytes2Disk.apply(JpegBytes2Disk.In.of(bytesPacket, outputFileOptions))
+ }
+
+ private fun createImageProxy(
+ takePictureRequest: TakePictureRequest,
+ ): ImageProxy {
+ // TODO - Take a bitmap as input and use that directly
+ val bitmap =
+ TestImageUtil.createBitmap(
+ takePictureRequest.cropRect.width(),
+ takePictureRequest.cropRect.height()
+ )
+ if (canLoadImageProcessingUtilJniLib()) {
+ val bytesPacket =
+ takePictureRequest.convertBitmapToBytes(
+ TestImageUtil.createBitmap(
+ takePictureRequest.cropRect.width(),
+ takePictureRequest.cropRect.height()
+ )
+ )
+ return jpegBytes2Image.apply(bytesPacket).data
+ } else {
+ return bitmap.toFakeImageProxy()
+ }
+ }
+
+ private fun Bitmap.toFakeImageProxy(): ImageProxy {
+ return FakeImageProxy(FakeImageInfo(), this)
+ }
+
+ private fun TakePictureRequest.convertBitmapToBytes(bitmap: Bitmap): Packet<ByteArray> {
+ val inputPacket =
+ Packet.of(
+ bitmap,
+ ExifUtil.createExif(
+ TestImageUtil.createJpegBytes(cropRect.width(), cropRect.height())
+ ),
+ cropRect,
+ rotationDegrees,
+ Matrix(),
+ FakeCameraCaptureResult()
+ )
+
+ return bitmap2JpegBytes.apply(Bitmap2JpegBytes.In.of(inputPacket, jpegQuality))
+ }
+
+ private fun canLoadImageProcessingUtilJniLib(): Boolean {
+ try {
+ System.loadLibrary(ImageProcessingUtil.JNI_LIB_NAME)
+ return true
+ } catch (e: UnsatisfiedLinkError) {
+ Logger.d(TAG, "canLoadImageProcessingUtilJniLib", e)
+ return false
+ }
+ }
+
+ private companion object {
+ private const val TAG = "TakePictureManagerWrap"
+ }
+}
diff --git a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraControlTest.java b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraControlTest.java
index a61ada7..d7264b0 100644
--- a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraControlTest.java
+++ b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraControlTest.java
@@ -173,6 +173,42 @@
}
@Test
+ public void notifiesLastRequestOnCaptureCompleted() {
+ CameraCaptureResult captureResult = new FakeCameraCaptureResult();
+
+ CountDownLatch latch = new CountDownLatch(1);
+ List<CameraCaptureResult> resultList = new ArrayList<>();
+ CaptureConfig captureConfig1 = createCaptureConfig(new CameraCaptureCallback() {
+ @Override
+ public void onCaptureCompleted(int captureConfigId,
+ @NonNull CameraCaptureResult cameraCaptureResult) {
+ resultList.add(cameraCaptureResult);
+ }
+ }, new CameraCaptureCallback() {
+ @Override
+ public void onCaptureCompleted(int captureConfigId,
+ @NonNull CameraCaptureResult cameraCaptureResult) {
+ resultList.add(cameraCaptureResult);
+ }
+ });
+ CaptureConfig captureConfig2 = createCaptureConfig(new CameraCaptureCallback() {
+ @Override
+ public void onCaptureCompleted(int captureConfigId,
+ @NonNull CameraCaptureResult cameraCaptureResult) {
+ resultList.add(cameraCaptureResult);
+ latch.countDown();
+ }
+ });
+
+ mCameraControl.submitStillCaptureRequests(Arrays.asList(captureConfig1, captureConfig2),
+ ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY, ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH);
+ mCameraControl.notifyLastRequestOnCaptureCompleted(captureResult);
+
+ awaitLatch(latch);
+ assertThat(resultList).containsExactlyElementsIn(Collections.singletonList(captureResult));
+ }
+
+ @Test
public void canUpdateFlashModeToOff() {
mCameraControl.setFlashMode(ImageCapture.FLASH_MODE_OFF);
assertThat(mCameraControl.getFlashMode()).isEqualTo(ImageCapture.FLASH_MODE_OFF);
@@ -319,7 +355,7 @@
List<CaptureConfig> notifiedCaptureConfigs = new ArrayList<>();
CountDownLatch latch = new CountDownLatch(1);
- mCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+ mCameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
notifiedCaptureConfigs.addAll(captureConfigs);
latch.countDown();
});
@@ -335,7 +371,7 @@
AtomicReference<Thread> listenerThread = new AtomicReference<>();
CountDownLatch latch = new CountDownLatch(1);
- mCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+ mCameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
listenerThread.set(Thread.currentThread());
latch.countDown();
});
@@ -350,7 +386,7 @@
AtomicReference<Thread> listenerThread = new AtomicReference<>();
CountDownLatch latch = new CountDownLatch(1);
- mCameraControl.setOnNewCaptureRequestListener(CameraXExecutors.mainThreadExecutor(),
+ mCameraControl.addOnNewCaptureRequestListener(CameraXExecutors.mainThreadExecutor(),
captureConfigs -> {
listenerThread.set(Thread.currentThread());
latch.countDown();
diff --git a/camera/camera-video/build.gradle b/camera/camera-video/build.gradle
index ed18c77..f992c47 100644
--- a/camera/camera-video/build.gradle
+++ b/camera/camera-video/build.gradle
@@ -53,8 +53,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.truth)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it's own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it's own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.autoValueAnnotations)
androidTestImplementation(project(":camera:camera-lifecycle"))
androidTestImplementation(project(":camera:camera-testing")) {
diff --git a/camera/camera-view/build.gradle b/camera/camera-view/build.gradle
index 251128f..fe349d6 100644
--- a/camera/camera-view/build.gradle
+++ b/camera/camera-view/build.gradle
@@ -70,8 +70,8 @@
}
androidTestImplementation(project(":camera:camera-camera2-pipe-integration"))
androidTestImplementation(project(":internal-testutils-truth"))
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it's own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it's own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation("androidx.test.espresso:espresso-core:3.3.0")
}
android {
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewDeviceTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewDeviceTest.kt
index c361fe0..3defd64b 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewDeviceTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewDeviceTest.kt
@@ -70,8 +70,6 @@
import com.google.common.util.concurrent.ListenableFuture
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executor
-import java.util.concurrent.ExecutorService
-import java.util.concurrent.Executors
import java.util.concurrent.Semaphore
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicReference
@@ -273,6 +271,8 @@
instrumentation.runOnMainSync {
val previewView = PreviewView(context)
+ // Specifies the content description and uses it to find the view to click
+ previewView.contentDescription = previewView.hashCode().toString()
clickEventHelper = ClickEventHelper(previewView)
previewView.setOnTouchListener(clickEventHelper)
previewView.controller = fakeController
@@ -311,7 +311,6 @@
private var uiDevice: UiDevice? = null
private var limitedRetryCount = 0
private var retriedCounter = 0
- private var executor: ExecutorService? = null
override fun onTouch(view: View, event: MotionEvent): Boolean {
if (view != targetView) {
@@ -337,7 +336,6 @@
uiDevice = null
limitedRetryCount = 0
retriedCounter = 0
- executor?.shutdown()
synchronized(lock) { isPerformingClick = false }
}
@@ -359,34 +357,16 @@
}
}
- executor = Executors.newSingleThreadExecutor()
limitedRetryCount = retryCount
retriedCounter = 0
this.uiDevice = uiDevice
performSingleClickInternal()
}
- private fun performSingleClickInternal() {
- executor!!.execute {
- var needClearContentDescription = false
- val originalContentDescription = targetView.contentDescription
-
- if (originalContentDescription == null || originalContentDescription.isEmpty()) {
- needClearContentDescription = true
- targetView.contentDescription = targetView.hashCode().toString()
- }
-
- uiDevice!!
- .findObject(
- UiSelector().descriptionContains(targetView.contentDescription.toString())
- )
- .click()
-
- if (needClearContentDescription) {
- targetView.contentDescription = originalContentDescription
- }
- }
- }
+ private fun performSingleClickInternal() =
+ uiDevice!!
+ .findObject(UiSelector().descriptionContains(targetView.hashCode().toString()))
+ .click()
}
@Test
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ResolutionSelectorDeviceTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ResolutionSelectorDeviceTest.kt
new file mode 100644
index 0000000..9e9562c
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ResolutionSelectorDeviceTest.kt
@@ -0,0 +1,692 @@
+/*
+ * 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.integration.core
+
+import android.Manifest
+import android.content.Context
+import android.graphics.ImageFormat
+import android.graphics.Point
+import android.graphics.SurfaceTexture
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES
+import android.util.Log
+import android.util.Range
+import android.util.Rational
+import android.util.Size
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.internal.DisplayInfoManager
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
+import androidx.camera.core.AspectRatio.RATIO_16_9
+import androidx.camera.core.AspectRatio.RATIO_4_3
+import androidx.camera.core.Camera
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraXConfig
+import androidx.camera.core.ImageAnalysis
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.Preview
+import androidx.camera.core.UseCase
+import androidx.camera.core.impl.CameraInfoInternal
+import androidx.camera.core.impl.ImageFormatConstants
+import androidx.camera.core.impl.ImageOutputConfig
+import androidx.camera.core.impl.Quirk
+import androidx.camera.core.impl.RestrictedCameraControl
+import androidx.camera.core.impl.utils.AspectRatioUtil
+import androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_16_9
+import androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_4_3
+import androidx.camera.core.impl.utils.AspectRatioUtil.hasMatchingAspectRatio
+import androidx.camera.core.impl.utils.CompareSizesByArea
+import androidx.camera.core.internal.utils.SizeUtil
+import androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_1080P
+import androidx.camera.core.resolutionselector.AspectRatioStrategy
+import androidx.camera.core.resolutionselector.AspectRatioStrategy.FALLBACK_RULE_AUTO
+import androidx.camera.core.resolutionselector.ResolutionFilter
+import androidx.camera.core.resolutionselector.ResolutionSelector
+import androidx.camera.core.resolutionselector.ResolutionSelector.PREFER_CAPTURE_RATE_OVER_HIGHER_RESOLUTION
+import androidx.camera.core.resolutionselector.ResolutionSelector.PREFER_HIGHER_RESOLUTION_OVER_CAPTURE_RATE
+import androidx.camera.core.resolutionselector.ResolutionStrategy
+import androidx.camera.core.resolutionselector.ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.impl.CameraPipeConfigTestRule
+import androidx.camera.testing.impl.CameraUtil
+import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
+import androidx.testutils.fail
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.TimeUnit
+import org.junit.After
+import org.junit.Assume.assumeFalse
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * ResolutionSelector related test on the real device.
+ *
+ * Make the ResolutionSelectorDeviceTest focus on the generic ResolutionSelector selection results
+ * for all the normal devices. Skips the tests when the devices have any of the quirks that might
+ * affect the selected resolution.
+ */
+@LargeTest
+@RunWith(Parameterized::class)
+@SdkSuppress(minSdkVersion = 21)
+class ResolutionSelectorDeviceTest(
+ private val implName: String,
+ private var cameraSelector: CameraSelector,
+ private val cameraConfig: CameraXConfig,
+) {
+ @get:Rule
+ val cameraPipeConfigTestRule =
+ CameraPipeConfigTestRule(
+ active = implName.contains(CameraPipeConfig::class.simpleName!!),
+ )
+
+ @get:Rule
+ val cameraRule =
+ CameraUtil.grantCameraPermissionAndPreTestAndPostTest(
+ CameraUtil.PreTestCameraIdList(cameraConfig)
+ )
+
+ @get:Rule
+ val permissionRule: GrantPermissionRule =
+ GrantPermissionRule.grant(Manifest.permission.RECORD_AUDIO)
+
+ private val useCaseFormatMap =
+ mapOf(
+ Pair(Preview::class.java, ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE),
+ Pair(ImageCapture::class.java, ImageFormat.JPEG),
+ Pair(ImageAnalysis::class.java, ImageFormat.YUV_420_888)
+ )
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun data() =
+ listOf(
+ arrayOf(
+ "back+" + Camera2Config::class.simpleName,
+ CameraSelector.DEFAULT_BACK_CAMERA,
+ Camera2Config.defaultConfig(),
+ ),
+ arrayOf(
+ "front+" + Camera2Config::class.simpleName,
+ CameraSelector.DEFAULT_FRONT_CAMERA,
+ Camera2Config.defaultConfig(),
+ ),
+ arrayOf(
+ "back+" + CameraPipeConfig::class.simpleName,
+ CameraSelector.DEFAULT_BACK_CAMERA,
+ CameraPipeConfig.defaultConfig(),
+ ),
+ arrayOf(
+ "front+" + CameraPipeConfig::class.simpleName,
+ CameraSelector.DEFAULT_FRONT_CAMERA,
+ CameraPipeConfig.defaultConfig(),
+ ),
+ )
+ }
+
+ private val instrumentation = InstrumentationRegistry.getInstrumentation()
+ private val context: Context = ApplicationProvider.getApplicationContext()
+ private lateinit var cameraProvider: ProcessCameraProvider
+ private lateinit var lifecycleOwner: FakeLifecycleOwner
+ private lateinit var camera: Camera
+ private lateinit var cameraInfoInternal: CameraInfoInternal
+
+ @Before
+ fun initializeCameraX() {
+ assumeTrue(CameraUtil.hasCameraWithLensFacing(cameraSelector.lensFacing!!))
+ ProcessCameraProvider.configureInstance(cameraConfig)
+ cameraProvider = ProcessCameraProvider.getInstance(context)[10, TimeUnit.SECONDS]
+
+ instrumentation.runOnMainSync {
+ lifecycleOwner = FakeLifecycleOwner()
+ lifecycleOwner.startAndResume()
+
+ camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector)
+ cameraInfoInternal = camera.cameraInfo as CameraInfoInternal
+ }
+
+ assumeNotAspectRatioQuirkDevice()
+ assumeNotOutputSizeQuirkDevice()
+ }
+
+ @After
+ fun shutdownCameraX() {
+ if (::cameraProvider.isInitialized) {
+ cameraProvider.shutdownAsync()[10, TimeUnit.SECONDS]
+ }
+ }
+
+ @Test
+ fun canSelect4x3ResolutionForPreviewImageCaptureAndImageAnalysis() {
+ canSelectTargetAspectRatioResolutionForPreviewImageCaptureAndImageAnalysis(RATIO_4_3)
+ }
+
+ @Test
+ fun canSelect16x9ResolutionForPreviewImageCaptureAndImageAnalysis() {
+ canSelectTargetAspectRatioResolutionForPreviewImageCaptureAndImageAnalysis(RATIO_16_9)
+ }
+
+ private fun canSelectTargetAspectRatioResolutionForPreviewImageCaptureAndImageAnalysis(
+ targetAspectRatio: Int
+ ) {
+ val preview = createUseCaseWithResolutionSelector(Preview::class.java, targetAspectRatio)
+ val imageCapture =
+ createUseCaseWithResolutionSelector(ImageCapture::class.java, targetAspectRatio)
+ val imageAnalysis =
+ createUseCaseWithResolutionSelector(ImageAnalysis::class.java, targetAspectRatio)
+ instrumentation.runOnMainSync {
+ cameraProvider.bindToLifecycle(
+ lifecycleOwner,
+ cameraSelector,
+ preview,
+ imageCapture,
+ imageAnalysis
+ )
+ }
+ assertThat(isResolutionAspectRatioBestMatched(preview, targetAspectRatio)).isTrue()
+ assertThat(isResolutionAspectRatioBestMatched(imageCapture, targetAspectRatio)).isTrue()
+ assertThat(isResolutionAspectRatioBestMatched(imageAnalysis, targetAspectRatio)).isTrue()
+ }
+
+ private fun isResolutionAspectRatioBestMatched(
+ useCase: UseCase,
+ targetAspectRatio: Int
+ ): Boolean {
+ val isMatched =
+ hasMatchingAspectRatio(
+ useCase.attachedSurfaceResolution!!,
+ aspectRatioToRational(targetAspectRatio)
+ )
+
+ if (isMatched) {
+ return true
+ }
+
+ // PRIV/PREVIEW + YUV/PREVIEW + JPEG/MAXIMUM will be used to select resolutions for the
+ // combination of Preview + ImageAnalysis + ImageCapture
+ val closestAspectRatioSizes =
+ if (useCase is Preview || useCase is ImageAnalysis) {
+ getClosestAspectRatioSizesUnderPreviewSize(targetAspectRatio, useCase.javaClass)
+ } else {
+ getClosestAspectRatioSizes(targetAspectRatio, useCase.javaClass)
+ }
+
+ Log.d(
+ "ResolutionSelectorDeviceTest",
+ "The selected resolution (${useCase.attachedSurfaceResolution!!}) does not exactly" +
+ " match the target aspect ratio. It is selected from the closest aspect ratio" +
+ " sizes: $closestAspectRatioSizes"
+ )
+
+ return closestAspectRatioSizes.contains(useCase.attachedSurfaceResolution!!)
+ }
+
+ @Test
+ fun canSelect4x3ResolutionForPreviewByResolutionStrategy() =
+ canSelectResolutionByResolutionStrategy(Preview::class.java, RATIO_4_3)
+
+ @Test
+ fun canSelect16x9ResolutionForPreviewByResolutionStrategy() =
+ canSelectResolutionByResolutionStrategy(Preview::class.java, RATIO_16_9)
+
+ @Test
+ fun canSelect4x3ResolutionForImageCaptureByResolutionStrategy() =
+ canSelectResolutionByResolutionStrategy(ImageCapture::class.java, RATIO_4_3)
+
+ @Test
+ fun canSelect16x9ResolutionForImageCaptureByResolutionStrategy() =
+ canSelectResolutionByResolutionStrategy(ImageCapture::class.java, RATIO_16_9)
+
+ @Test
+ fun canSelect4x3ResolutionForImageAnalysisByResolutionStrategy() =
+ canSelectResolutionByResolutionStrategy(ImageAnalysis::class.java, RATIO_4_3)
+
+ @Test
+ fun canSelect16x9ResolutionForImageAnalysisByResolutionStrategy() =
+ canSelectResolutionByResolutionStrategy(ImageAnalysis::class.java, RATIO_16_9)
+
+ private fun <T : UseCase> canSelectResolutionByResolutionStrategy(
+ useCaseClass: Class<T>,
+ ratio: Int
+ ) {
+ // Filters the output sizes matching the target aspect ratio
+ cameraInfoInternal
+ .getSupportedResolutions(useCaseFormatMap[useCaseClass]!!)
+ .filter { size -> hasMatchingAspectRatio(size, aspectRatioToRational(ratio)) }
+ .let {
+ // Picks the item in the middle of the list to run the test
+ it.elementAtOrNull(it.size / 2)?.let { boundSize ->
+ {
+ val useCase =
+ createUseCaseWithResolutionSelector(
+ useCaseClass,
+ aspectRatio = ratio,
+ aspectRatioStrategyFallbackRule = FALLBACK_RULE_AUTO,
+ boundSize = boundSize,
+ resolutionStrategyFallbackRule =
+ FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER
+ )
+ instrumentation.runOnMainSync {
+ cameraProvider.unbindAll()
+ cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, useCase)
+ }
+ assertThat(useCase.attachedSurfaceResolution).isEqualTo(boundSize)
+ }
+ }
+ }
+ }
+
+ @Test
+ fun canSelectAnyResolutionForPreviewByResolutionFilter() =
+ canSelectAnyResolutionByResolutionFilter(
+ Preview::class.java,
+ // For Preview, need to override resolution strategy so that the output sizes larger
+ // than PREVIEW size can be selected.
+ cameraInfoInternal
+ .getSupportedResolutions(useCaseFormatMap[Preview::class.java]!!)
+ .maxWithOrNull(CompareSizesByArea())
+ )
+
+ @Test
+ fun canSelectAnyHighResolutionForPreviewByResolutionFilter() =
+ canSelectAnyHighResolutionByResolutionFilter(
+ Preview::class.java,
+ // For Preview, need to override resolution strategy so that the output sizes larger
+ // than PREVIEW size can be selected.
+ cameraInfoInternal
+ .getSupportedHighResolutions(useCaseFormatMap[Preview::class.java]!!)
+ .maxWithOrNull(CompareSizesByArea())
+ )
+
+ @Test
+ fun canSelectAnyResolutionForImageCaptureByResolutionFilter() =
+ canSelectAnyResolutionByResolutionFilter(ImageCapture::class.java)
+
+ @Test
+ fun canSelectAnyHighResolutionForImageCaptureByResolutionFilter() =
+ canSelectAnyHighResolutionByResolutionFilter(ImageCapture::class.java)
+
+ @Test
+ fun canSelectAnyResolutionForImageAnalysisByResolutionFilter() =
+ canSelectAnyResolutionByResolutionFilter(ImageAnalysis::class.java)
+
+ @Test
+ fun canSelectAnyHighResolutionForImageAnalysisByResolutionFilter() =
+ canSelectAnyHighResolutionByResolutionFilter(ImageAnalysis::class.java)
+
+ private fun <T : UseCase> canSelectAnyResolutionByResolutionFilter(
+ useCaseClass: Class<T>,
+ boundSize: Size? = null,
+ resolutionStrategyFallbackRule: Int = FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER
+ ) =
+ canSelectAnyResolutionByResolutionFilter(
+ useCaseClass,
+ cameraInfoInternal.getSupportedResolutions(useCaseFormatMap[useCaseClass]!!),
+ boundSize,
+ resolutionStrategyFallbackRule
+ )
+
+ private fun <T : UseCase> canSelectAnyHighResolutionByResolutionFilter(
+ useCaseClass: Class<T>,
+ boundSize: Size? = null,
+ resolutionStrategyFallbackRule: Int = FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER
+ ) =
+ canSelectAnyResolutionByResolutionFilter(
+ useCaseClass,
+ cameraInfoInternal.getSupportedHighResolutions(useCaseFormatMap[useCaseClass]!!),
+ boundSize,
+ resolutionStrategyFallbackRule,
+ PREFER_HIGHER_RESOLUTION_OVER_CAPTURE_RATE
+ )
+
+ private fun <T : UseCase> canSelectAnyResolutionByResolutionFilter(
+ useCaseClass: Class<T>,
+ outputSizes: List<Size>,
+ boundSize: Size? = null,
+ resolutionStrategyFallbackRule: Int = FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER,
+ allowedResolutionMode: Int = PREFER_CAPTURE_RATE_OVER_HIGHER_RESOLUTION
+ ) {
+ outputSizes.forEach { targetResolution ->
+ val useCase =
+ createUseCaseWithResolutionSelector(
+ useCaseClass,
+ boundSize = boundSize,
+ resolutionStrategyFallbackRule = resolutionStrategyFallbackRule,
+ resolutionFilter = { _, _ -> mutableListOf(targetResolution) },
+ allowedResolutionMode = allowedResolutionMode
+ )
+ instrumentation.runOnMainSync {
+ cameraProvider.unbindAll()
+ cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, useCase)
+ }
+ assertThat(useCase.attachedSurfaceResolution).isEqualTo(targetResolution)
+ }
+ }
+
+ @Test
+ fun canSelectResolutionForSixtyFpsPreview() {
+ assumeTrue(isSixtyFpsSupported())
+
+ val preview = Preview.Builder().setTargetFrameRate(Range.create(60, 60)).build()
+ val imageCapture = ImageCapture.Builder().build()
+
+ instrumentation.runOnMainSync {
+ cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageCapture)
+ }
+
+ assertThat(getMaxFrameRate(preview.attachedSurfaceResolution!!)).isEqualTo(60)
+ }
+
+ private fun isSixtyFpsSupported() =
+ CameraUtil.getCameraCharacteristics(cameraSelector.lensFacing!!)
+ ?.get(CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES)
+ ?.any { range -> range.upper == 60 } ?: false
+
+ private fun getMaxFrameRate(size: Size) =
+ (1_000_000_000.0 /
+ CameraUtil.getCameraCharacteristics(cameraSelector.lensFacing!!)!!.get(
+ CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
+ )!!
+ .getOutputMinFrameDuration(SurfaceTexture::class.java, size) + 0.5)
+ .toInt()
+
+ private fun <T : UseCase> createUseCaseWithResolutionSelector(
+ useCaseClass: Class<T>,
+ aspectRatio: Int? = null,
+ aspectRatioStrategyFallbackRule: Int = FALLBACK_RULE_AUTO,
+ boundSize: Size? = null,
+ resolutionStrategyFallbackRule: Int = FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER,
+ resolutionFilter: ResolutionFilter? = null,
+ allowedResolutionMode: Int = PREFER_CAPTURE_RATE_OVER_HIGHER_RESOLUTION
+ ): UseCase {
+ val builder =
+ when (useCaseClass) {
+ Preview::class.java -> Preview.Builder()
+ ImageCapture::class.java -> ImageCapture.Builder()
+ ImageAnalysis::class.java -> ImageAnalysis.Builder()
+ else -> throw IllegalArgumentException("Unsupported class type!!")
+ }
+
+ (builder as ImageOutputConfig.Builder<*>).setResolutionSelector(
+ createResolutionSelector(
+ aspectRatio,
+ aspectRatioStrategyFallbackRule,
+ boundSize,
+ resolutionStrategyFallbackRule,
+ resolutionFilter,
+ allowedResolutionMode
+ )
+ )
+
+ return builder.build()
+ }
+
+ private fun createResolutionSelector(
+ aspectRatio: Int? = null,
+ aspectRatioFallbackRule: Int = FALLBACK_RULE_AUTO,
+ boundSize: Size? = null,
+ resolutionFallbackRule: Int = FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER,
+ resolutionFilter: ResolutionFilter? = null,
+ allowedResolutionMode: Int = PREFER_CAPTURE_RATE_OVER_HIGHER_RESOLUTION
+ ) =
+ ResolutionSelector.Builder()
+ .apply {
+ aspectRatio?.let {
+ setAspectRatioStrategy(
+ AspectRatioStrategy(aspectRatio, aspectRatioFallbackRule)
+ )
+ }
+ boundSize?.let {
+ setResolutionStrategy(ResolutionStrategy(boundSize, resolutionFallbackRule))
+ }
+ resolutionFilter?.let { setResolutionFilter(resolutionFilter) }
+ setAllowedResolutionMode(allowedResolutionMode)
+ }
+ .build()
+
+ private fun aspectRatioToRational(ratio: Int) =
+ if (ratio == RATIO_16_9) {
+ ASPECT_RATIO_16_9
+ } else {
+ ASPECT_RATIO_4_3
+ }
+
+ private fun <T : UseCase> getClosestAspectRatioSizesUnderPreviewSize(
+ targetAspectRatio: Int,
+ useCaseClass: Class<T>
+ ): List<Size> {
+ val outputSizes =
+ cameraInfoInternal.getSupportedResolutions(useCaseFormatMap[useCaseClass]!!)
+ return outputSizes
+ .getSmallerThanOrEqualToPreviewScaleSizeSublist()
+ .getClosestAspectRatioSublist(targetAspectRatio)
+ }
+
+ private fun <T : UseCase> getClosestAspectRatioSizes(
+ targetAspectRatio: Int,
+ useCaseClass: Class<T>
+ ): List<Size> {
+ val outputSizes =
+ cameraInfoInternal.getSupportedResolutions(useCaseFormatMap[useCaseClass]!!)
+ return outputSizes.getClosestAspectRatioSublist(targetAspectRatio)
+ }
+
+ private fun List<Size>.getSmallerThanOrEqualToPreviewScaleSizeSublist() = filter { size ->
+ SizeUtil.getArea(size) <= SizeUtil.getArea(getPreviewScaleSize())
+ }
+
+ @Suppress("DEPRECATION")
+ private fun getPreviewScaleSize(): Size {
+ val point = Point()
+ DisplayInfoManager.getInstance(context).getMaxSizeDisplay(false).getRealSize(point)
+ val displaySize = Size(point.x, point.y)
+ return if (SizeUtil.isSmallerByArea(RESOLUTION_1080P, displaySize)) {
+ RESOLUTION_1080P
+ } else {
+ displaySize
+ }
+ }
+
+ private fun List<Size>.getClosestAspectRatioSublist(targetAspectRatio: Int): List<Size> {
+ val sensorRect = (camera.cameraControl as RestrictedCameraControl).sensorRect
+ val aspectRatios = getResolutionListGroupingAspectRatioKeys(this)
+ val sortedAspectRatios =
+ aspectRatios.sortedWith(
+ AspectRatioUtil.CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace(
+ aspectRatioToRational(targetAspectRatio),
+ Rational(sensorRect.width(), sensorRect.height())
+ )
+ )
+ val groupedRatioToSizesMap = groupSizesByAspectRatio(this)
+
+ for (ratio in sortedAspectRatios) {
+ groupedRatioToSizesMap[ratio]?.let {
+ if (it.isNotEmpty()) {
+ return it
+ }
+ }
+ }
+
+ fail("There should have one non-empty size list returned.")
+ }
+
+ /**
+ * Returns the grouping aspect ratio keys of the input resolution list.
+ *
+ * Some sizes might be mod16 case. When grouping, those sizes will be grouped into an existing
+ * aspect ratio group if the aspect ratio can match by the mod16 rule.
+ */
+ private fun getResolutionListGroupingAspectRatioKeys(
+ resolutionCandidateList: List<Size>
+ ): List<Rational> {
+ val aspectRatios = mutableListOf<Rational>()
+
+ // Adds the default 4:3 and 16:9 items first to avoid their mod16 sizes to create
+ // additional items.
+ aspectRatios.add(ASPECT_RATIO_4_3)
+ aspectRatios.add(ASPECT_RATIO_16_9)
+
+ // Tries to find the aspect ratio which the target size belongs to.
+ for (size in resolutionCandidateList) {
+ val newRatio = Rational(size.width, size.height)
+ val aspectRatioFound = aspectRatios.contains(newRatio)
+
+ // The checking size might be a mod16 size which can be mapped to an existing aspect
+ // ratio group.
+ if (!aspectRatioFound) {
+ var hasMatchingAspectRatio = false
+ for (aspectRatio in aspectRatios) {
+ if (hasMatchingAspectRatio(size, aspectRatio)) {
+ hasMatchingAspectRatio = true
+ break
+ }
+ }
+ if (!hasMatchingAspectRatio) {
+ aspectRatios.add(newRatio)
+ }
+ }
+ }
+
+ return aspectRatios
+ }
+
+ /** Groups the input sizes into an aspect ratio to size list map. */
+ private fun groupSizesByAspectRatio(sizes: List<Size>): Map<Rational, MutableList<Size>> {
+ val aspectRatioSizeListMap = mutableMapOf<Rational, MutableList<Size>>()
+ val aspectRatioKeys = getResolutionListGroupingAspectRatioKeys(sizes)
+
+ for (aspectRatio in aspectRatioKeys) {
+ aspectRatioSizeListMap[aspectRatio] = mutableListOf()
+ }
+
+ for (outputSize in sizes) {
+ for (key in aspectRatioSizeListMap.keys) {
+ // Put the size into all groups that is matched in mod16 condition since a size
+ // may match multiple aspect ratio in mod16 algorithm.
+ if (hasMatchingAspectRatio(outputSize, key)) {
+ aspectRatioSizeListMap[key]!!.add(outputSize)
+ }
+ }
+ }
+
+ return aspectRatioSizeListMap
+ }
+
+ // Skips the tests when the devices have any of the quirks that might affect the selected
+ // resolution.
+ private fun assumeNotAspectRatioQuirkDevice() {
+ assumeFalse(hasAspectRatioLegacyApi21Quirk())
+ assumeFalse(hasNexus4AndroidLTargetAspectRatioQuirk())
+ assumeFalse(hasExtraCroppingQuirk())
+ }
+
+ // Checks whether it is the device for AspectRatioLegacyApi21Quirk
+ private fun hasAspectRatioLegacyApi21Quirk(): Boolean {
+ val quirks = cameraInfoInternal.cameraQuirks
+
+ return if (implName == CameraPipeConfig::class.simpleName) {
+ quirks.contains(
+ androidx.camera.camera2.pipe.integration.compat.quirk
+ .AspectRatioLegacyApi21Quirk::class
+ .java
+ )
+ } else {
+ quirks.contains(
+ androidx.camera.camera2.internal.compat.quirk.AspectRatioLegacyApi21Quirk::class
+ .java
+ )
+ }
+ }
+
+ // Checks whether it is the device for Nexus4AndroidLTargetAspectRatioQuirk
+ private fun hasNexus4AndroidLTargetAspectRatioQuirk() =
+ if (implName == CameraPipeConfig::class.simpleName) {
+ hasDeviceQuirk(
+ androidx.camera.camera2.pipe.integration.compat.quirk
+ .Nexus4AndroidLTargetAspectRatioQuirk::class
+ .java
+ )
+ } else {
+ hasDeviceQuirk(
+ androidx.camera.camera2.internal.compat.quirk
+ .Nexus4AndroidLTargetAspectRatioQuirk::class
+ .java
+ )
+ }
+
+ // Checks whether it is the device for ExtraCroppingQuirk
+ private fun hasExtraCroppingQuirk() =
+ if (implName == CameraPipeConfig::class.simpleName) {
+ hasDeviceQuirk(
+ androidx.camera.camera2.pipe.integration.compat.quirk.ExtraCroppingQuirk::class.java
+ )
+ } else {
+ hasDeviceQuirk(
+ androidx.camera.camera2.internal.compat.quirk.ExtraCroppingQuirk::class.java
+ )
+ }
+
+ // Skips the tests when the devices have any of the quirks that might affect the selected
+ // resolution.
+ private fun assumeNotOutputSizeQuirkDevice() {
+ assumeFalse(hasExcludedSupportedSizesQuirk())
+ assumeFalse(hasExtraSupportedOutputSizeQuirk())
+ }
+
+ private fun hasExcludedSupportedSizesQuirk() =
+ if (implName == CameraPipeConfig::class.simpleName) {
+ hasDeviceQuirk(
+ androidx.camera.camera2.pipe.integration.compat.quirk
+ .ExcludedSupportedSizesQuirk::class
+ .java
+ )
+ } else {
+ hasDeviceQuirk(
+ androidx.camera.camera2.internal.compat.quirk.ExcludedSupportedSizesQuirk::class
+ .java
+ )
+ }
+
+ private fun hasExtraSupportedOutputSizeQuirk() =
+ if (implName == CameraPipeConfig::class.simpleName) {
+ hasDeviceQuirk(
+ androidx.camera.camera2.pipe.integration.compat.quirk
+ .ExtraSupportedOutputSizeQuirk::class
+ .java
+ )
+ } else {
+ hasDeviceQuirk(
+ androidx.camera.camera2.internal.compat.quirk.ExtraSupportedOutputSizeQuirk::class
+ .java
+ )
+ }
+
+ private fun <T : Quirk?> hasDeviceQuirk(quirkClass: Class<T>) =
+ if (implName == CameraPipeConfig::class.simpleName) {
+ androidx.camera.camera2.pipe.integration.compat.quirk.DeviceQuirks.get(quirkClass)
+ } else {
+ androidx.camera.camera2.internal.compat.quirk.DeviceQuirks.get(quirkClass)
+ } != null
+}
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/fakecamera/ImageCaptureTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/fakecamera/ImageCaptureTest.kt
index af52c44..ef4fe9e 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/fakecamera/ImageCaptureTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/fakecamera/ImageCaptureTest.kt
@@ -42,7 +42,6 @@
import kotlinx.coroutines.withContext
import org.junit.After
import org.junit.Before
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
@@ -87,7 +86,7 @@
@Test
fun canSubmitTakePictureRequest(): Unit = runBlocking {
val countDownLatch = CountDownLatch(1)
- cameraControl.setOnNewCaptureRequestListener { countDownLatch.countDown() }
+ cameraControl.addOnNewCaptureRequestListener { countDownLatch.countDown() }
imageCapture.takePicture(CameraXExecutors.directExecutor(), FakeOnImageCapturedCallback())
@@ -96,10 +95,9 @@
// Duplicate to ImageCaptureTest on core-test-app JVM tests, any change here may need to be
// reflected there too
- @Ignore("b/318314454")
@Test
fun canCreateBitmapFromTakenImage_whenImageCapturedCallbackIsUsed(): Unit = runBlocking {
- val callback = FakeOnImageCapturedCallback()
+ val callback = FakeOnImageCapturedCallback(closeImageOnSuccess = false)
imageCapture.takePicture(CameraXExecutors.directExecutor(), callback)
callback.awaitCapturesAndAssert(capturedImagesCount = 1)
callback.results.first().image.toBitmap()
@@ -107,7 +105,6 @@
// Duplicate to ImageCaptureTest on core-test-app JVM tests, any change here may need to be
// reflected there too
- @Ignore("b/318314454")
@Test
fun canFindImage_whenFileStorageAndImageSavedCallbackIsUsed(): Unit = runBlocking {
val saveLocation = temporaryFolder.newFile()
@@ -126,7 +123,6 @@
// Duplicate to ImageCaptureTest on androidTest/fakecamera/ImageCaptureTest, any change here may
// need to be reflected there too
- @Ignore("b/318314454")
@Test
fun canFindImage_whenMediaStoreAndImageSavedCallbackIsUsed(): Unit = runBlocking {
val initialCount = getMediaStoreCameraXImageCount()
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java
index 9e7449f..0f56828 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java
@@ -32,6 +32,8 @@
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.appcompat.app.AppCompatActivity;
+import androidx.camera.camera2.Camera2Config;
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig;
import androidx.camera.core.Camera;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.Logger;
@@ -77,6 +79,13 @@
private static final String VIDEO_FILE_PREFIX = "video";
private static final String INFO_FILE_PREFIX = "video_camera_switching_test_info";
private static final String KEY_DEVICE_ORIENTATION = "device_orientation";
+ private static final String INTENT_EXTRA_CAMERA_IMPLEMENTATION = "camera_implementation";
+ // Camera2 implementation.
+ private static final String CAMERA2_IMPLEMENTATION_OPTION = "camera2";
+ // Camera-pipe implementation.
+ private static final String CAMERA_PIPE_IMPLEMENTATION_OPTION = "camera_pipe";
+
+ private static String sCameraImplementationType;
@NonNull
private CameraSelector mCameraSelector = CameraSelector.DEFAULT_BACK_CAMERA;
@@ -103,6 +112,7 @@
private OrientationEventListener mOrientationEventListener;
private int mMirrorMode = MIRROR_MODE_OFF;
+ @OptIn(markerClass = androidx.camera.lifecycle.ExperimentalCameraProviderConfiguration.class)
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -130,6 +140,21 @@
} else {
mMirrorMode = MIRROR_MODE_OFF;
}
+
+ String cameraImplementation = bundle.getString(INTENT_EXTRA_CAMERA_IMPLEMENTATION);
+ if (cameraImplementation != null && sCameraImplementationType == null) {
+ if (cameraImplementation.equals(CAMERA2_IMPLEMENTATION_OPTION)) {
+ ProcessCameraProvider.configureInstance(Camera2Config.defaultConfig());
+ sCameraImplementationType = cameraImplementation;
+ } else if (cameraImplementation.equals(CAMERA_PIPE_IMPLEMENTATION_OPTION)) {
+ ProcessCameraProvider.configureInstance(
+ CameraPipeConfig.defaultConfig());
+ sCameraImplementationType = cameraImplementation;
+ } else {
+ throw new IllegalArgumentException("Failed to configure the CameraProvider "
+ + "using unknown " + cameraImplementation + " implementation option.");
+ }
+ }
}
mOrientationEventListener = new OrientationEventListener(this) {
diff --git a/camera/integration-tests/coretestapp/src/test/java/androidx/camera/integration/core/ImageCaptureTest.kt b/camera/integration-tests/coretestapp/src/test/java/androidx/camera/integration/core/ImageCaptureTest.kt
index 459187c..15cbefe 100644
--- a/camera/integration-tests/coretestapp/src/test/java/androidx/camera/integration/core/ImageCaptureTest.kt
+++ b/camera/integration-tests/coretestapp/src/test/java/androidx/camera/integration/core/ImageCaptureTest.kt
@@ -44,7 +44,6 @@
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
@@ -92,7 +91,7 @@
@Test
fun canSubmitTakePictureRequest(): Unit = runTest {
val countDownLatch = CountDownLatch(1)
- cameraControl.setOnNewCaptureRequestListener { countDownLatch.countDown() }
+ cameraControl.addOnNewCaptureRequestListener { countDownLatch.countDown() }
imageCapture.takePicture(CameraXExecutors.directExecutor(), FakeOnImageCapturedCallback())
@@ -101,7 +100,6 @@
// Duplicate to ImageCaptureTest on androidTest/fakecamera/ImageCaptureTest, any change here may
// need to be reflected there too
- @Ignore("b/318314454")
@Test
fun canCreateBitmapFromTakenImage_whenImageCapturedCallbackIsUsed(): Unit = runTest {
val callback = FakeOnImageCapturedCallback()
@@ -112,7 +110,6 @@
// Duplicate to ImageCaptureTest on androidTest/fakecamera/ImageCaptureTest, any change here may
// need to be reflected there too
- @Ignore("b/318314454")
@Test
fun canFindImage_whenFileStorageAndImageSavedCallbackIsUsed(): Unit = runTest {
val saveLocation = temporaryFolder.newFile()
@@ -131,7 +128,6 @@
// Duplicate to ImageCaptureTest on androidTest/fakecamera/ImageCaptureTest, any change here may
// need to be reflected there too
- @Ignore("b/318314454")
@Test
fun canFindFakeImageUri_whenMediaStoreAndImageSavedCallbackIsUsed(): Unit = runBlocking {
val callback = FakeOnImageSavedCallback()
diff --git a/camera/integration-tests/viewtestapp/build.gradle b/camera/integration-tests/viewtestapp/build.gradle
index 4cff2d8..6a72f25 100644
--- a/camera/integration-tests/viewtestapp/build.gradle
+++ b/camera/integration-tests/viewtestapp/build.gradle
@@ -56,6 +56,7 @@
implementation(project(":camera:camera-view"))
implementation(project(":camera:camera-video"))
implementation(project(":camera:camera-effects"))
+ implementation(project(":camera:camera-media3-effect"))
implementation(libs.guavaAndroid)
implementation('com.google.mlkit:barcode-scanning:17.0.2')
implementation("androidx.exifinterface:exifinterface:1.3.2")
@@ -78,6 +79,8 @@
implementation("androidx.compose.material:material:1.4.0")
implementation("androidx.compose.ui:ui:1.4.0")
implementation("androidx.compose.foundation:foundation:1.4.0")
+ implementation(libs.media3Common)
+ implementation(libs.media3Effect)
// Align dependencies in debugRuntimeClasspath and debugAndroidTestRuntimeClasspath.
androidTestImplementation("androidx.annotation:annotation-experimental:1.4.1")
diff --git a/camera/viewfinder/viewfinder-view/build.gradle b/camera/viewfinder/viewfinder-view/build.gradle
index 4225bde..b2854e8 100644
--- a/camera/viewfinder/viewfinder-view/build.gradle
+++ b/camera/viewfinder/viewfinder-view/build.gradle
@@ -62,8 +62,8 @@
androidTestImplementation(libs.testUiautomator)
androidTestImplementation(libs.kotlinStdlib)
androidTestImplementation(libs.truth)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it's own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it's own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
}
android {
diff --git a/car/app/app/api/1.7.0-beta02.txt b/car/app/app/api/1.7.0-beta02.txt
index 2bc33d2..6939bce 100644
--- a/car/app/app/api/1.7.0-beta02.txt
+++ b/car/app/app/api/1.7.0-beta02.txt
@@ -900,6 +900,7 @@
field public static final String KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED";
field public static final String KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW";
field public static final String KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED";
+ field public static final String KEY_ROOT_HINT_MEDIA_HOST_VERSION = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_HOST_VERSION";
field public static final String KEY_ROOT_HINT_MEDIA_SESSION_API = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_SESSION_API";
}
diff --git a/car/app/app/api/current.ignore b/car/app/app/api/current.ignore
index 0ba421d..e1d8307 100644
--- a/car/app/app/api/current.ignore
+++ b/car/app/app/api/current.ignore
@@ -1,3 +1,3 @@
// Baseline format: 1.0
-AddedClass: androidx.car.app.mediaextensions.MediaIntentExtras:
- Added class androidx.car.app.mediaextensions.MediaIntentExtras
+AddedField: androidx.car.app.mediaextensions.MediaBrowserExtras#KEY_ROOT_HINT_MEDIA_HOST_VERSION:
+ Added field androidx.car.app.mediaextensions.MediaBrowserExtras.KEY_ROOT_HINT_MEDIA_HOST_VERSION
diff --git a/car/app/app/api/current.txt b/car/app/app/api/current.txt
index 2bc33d2..6939bce 100644
--- a/car/app/app/api/current.txt
+++ b/car/app/app/api/current.txt
@@ -900,6 +900,7 @@
field public static final String KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED";
field public static final String KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW";
field public static final String KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED";
+ field public static final String KEY_ROOT_HINT_MEDIA_HOST_VERSION = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_HOST_VERSION";
field public static final String KEY_ROOT_HINT_MEDIA_SESSION_API = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_SESSION_API";
}
diff --git a/car/app/app/api/restricted_1.7.0-beta02.txt b/car/app/app/api/restricted_1.7.0-beta02.txt
index 2bc33d2..6939bce 100644
--- a/car/app/app/api/restricted_1.7.0-beta02.txt
+++ b/car/app/app/api/restricted_1.7.0-beta02.txt
@@ -900,6 +900,7 @@
field public static final String KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED";
field public static final String KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW";
field public static final String KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED";
+ field public static final String KEY_ROOT_HINT_MEDIA_HOST_VERSION = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_HOST_VERSION";
field public static final String KEY_ROOT_HINT_MEDIA_SESSION_API = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_SESSION_API";
}
diff --git a/car/app/app/api/restricted_current.ignore b/car/app/app/api/restricted_current.ignore
index 0ba421d..e1d8307 100644
--- a/car/app/app/api/restricted_current.ignore
+++ b/car/app/app/api/restricted_current.ignore
@@ -1,3 +1,3 @@
// Baseline format: 1.0
-AddedClass: androidx.car.app.mediaextensions.MediaIntentExtras:
- Added class androidx.car.app.mediaextensions.MediaIntentExtras
+AddedField: androidx.car.app.mediaextensions.MediaBrowserExtras#KEY_ROOT_HINT_MEDIA_HOST_VERSION:
+ Added field androidx.car.app.mediaextensions.MediaBrowserExtras.KEY_ROOT_HINT_MEDIA_HOST_VERSION
diff --git a/car/app/app/api/restricted_current.txt b/car/app/app/api/restricted_current.txt
index 2bc33d2..6939bce 100644
--- a/car/app/app/api/restricted_current.txt
+++ b/car/app/app/api/restricted_current.txt
@@ -900,6 +900,7 @@
field public static final String KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED";
field public static final String KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW";
field public static final String KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED";
+ field public static final String KEY_ROOT_HINT_MEDIA_HOST_VERSION = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_HOST_VERSION";
field public static final String KEY_ROOT_HINT_MEDIA_SESSION_API = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_SESSION_API";
}
diff --git a/car/app/app/src/main/java/androidx/car/app/mediaextensions/MediaBrowserExtras.java b/car/app/app/src/main/java/androidx/car/app/mediaextensions/MediaBrowserExtras.java
index 473f447..413cd71 100644
--- a/car/app/app/src/main/java/androidx/car/app/mediaextensions/MediaBrowserExtras.java
+++ b/car/app/app/src/main/java/androidx/car/app/mediaextensions/MediaBrowserExtras.java
@@ -37,6 +37,17 @@
/**
* {@link Bundle} key used in the rootHints bundle passed to
+ * {@link androidx.media.MediaBrowserServiceCompat#onGetRoot(String, int, Bundle)}
+ * to indicate the version of the caller. Note that this should only be used for analytics and
+ * is different than {@link #KEY_ROOT_HINT_MEDIA_SESSION_API}.
+ *
+ * <p>TYPE: string - the version info.
+ */
+ public static final String KEY_ROOT_HINT_MEDIA_HOST_VERSION =
+ "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_HOST_VERSION";
+
+ /**
+ * {@link Bundle} key used in the rootHints bundle passed to
* {@link androidx.media.MediaBrowserServiceCompat#onGetRoot(String, int, Bundle)} to indicate
* which version of the media api is used by the caller
*
diff --git a/compose/animation/animation-core/build.gradle b/compose/animation/animation-core/build.gradle
index 595aef5..a0f829e 100644
--- a/compose/animation/animation-core/build.gradle
+++ b/compose/animation/animation-core/build.gradle
@@ -54,6 +54,9 @@
commonTest {
dependencies {
+ implementation(kotlin("test"))
+ implementation(libs.kotlinCoroutinesTest)
+ implementation(project(":kruth:kruth"))
}
}
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/AnimatableTest.kt b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/AnimatableTest.kt
deleted file mode 100644
index c09ec81..0000000
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/AnimatableTest.kt
+++ /dev/null
@@ -1,392 +0,0 @@
-/*
- * Copyright 2020 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.animation.core
-
-import androidx.compose.runtime.MonotonicFrameClock
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.unit.IntSize
-import com.google.common.truth.Truth.assertThat
-import junit.framework.TestCase.assertEquals
-import junit.framework.TestCase.assertFalse
-import junit.framework.TestCase.assertTrue
-import kotlin.math.abs
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.withContext
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class AnimatableTest {
- @Test
- fun animateDecayTest() {
- runBlocking {
- val from = 9f
- val initialVelocity = 20f
- val decaySpec = FloatExponentialDecaySpec()
- val anim =
- DecayAnimation(decaySpec, initialValue = from, initialVelocity = initialVelocity)
- val clock = SuspendAnimationTest.TestFrameClock()
- val interval = 50
- withContext(clock) {
- // Put in a bunch of frames 50 milliseconds apart
- for (frameTimeMillis in 0..5000 step interval) {
- clock.frame(frameTimeMillis * 1_000_000L)
- }
- var playTimeMillis = 0L
- val animatable = Animatable(9f)
- val result =
- animatable.animateDecay(20f, animationSpec = exponentialDecay()) {
- assertTrue(isRunning)
- assertEquals(anim.targetValue, targetValue)
- assertEquals(anim.getValueFromMillis(playTimeMillis), value, 0.001f)
- assertEquals(anim.getVelocityFromMillis(playTimeMillis), velocity, 0.001f)
- playTimeMillis += interval
- assertEquals(value, animatable.value, 0.0001f)
- assertEquals(velocity, animatable.velocity, 0.0001f)
- }
- // After animation
- assertEquals(anim.targetValue, animatable.value)
- assertEquals(false, animatable.isRunning)
- assertEquals(0f, animatable.velocity)
- assertEquals(AnimationEndReason.Finished, result.endReason)
- assertTrue(abs(result.endState.velocity) <= decaySpec.absVelocityThreshold)
- }
- }
- }
-
- @Test
- fun animateToTest() {
- runBlocking {
- val anim =
- TargetBasedAnimation(
- spring(dampingRatio = Spring.DampingRatioMediumBouncy),
- Float.VectorConverter,
- initialValue = 0f,
- targetValue = 1f
- )
- val clock = SuspendAnimationTest.TestFrameClock()
- val interval = 50
- val animatable = Animatable(0f)
- withContext(clock) {
- // Put in a bunch of frames 50 milliseconds apart
- for (frameTimeMillis in 0..5000 step interval) {
- clock.frame(frameTimeMillis * 1_000_000L)
- }
- var playTimeMillis = 0L
- val result =
- animatable.animateTo(
- 1f,
- spring(dampingRatio = Spring.DampingRatioMediumBouncy)
- ) {
- assertTrue(isRunning)
- assertEquals(1f, targetValue)
- assertEquals(anim.getValueFromMillis(playTimeMillis), value, 0.001f)
- assertEquals(anim.getVelocityFromMillis(playTimeMillis), velocity, 0.001f)
- playTimeMillis += interval
- }
- // After animation
- assertEquals(anim.targetValue, animatable.value)
- assertEquals(0f, animatable.velocity)
- assertEquals(false, animatable.isRunning)
- assertEquals(AnimationEndReason.Finished, result.endReason)
- }
- }
- }
-
- @Test
- fun animateToGenericTypeTest() =
- runBlocking<Unit> {
- val from = Offset(666f, 321f)
- val to = Offset(919f, 864f)
- val offsetToVector: TwoWayConverter<Offset, AnimationVector2D> =
- TwoWayConverter(
- convertToVector = { AnimationVector2D(it.x, it.y) },
- convertFromVector = { Offset(it.v1, it.v2) }
- )
- val anim =
- TargetBasedAnimation(
- tween(500),
- offsetToVector,
- initialValue = from,
- targetValue = to
- )
- val clock = SuspendAnimationTest.TestFrameClock()
- val interval = 50
- val animatable = Animatable(initialValue = from, typeConverter = offsetToVector)
- coroutineScope {
- withContext(clock) {
- launch {
- // Put in a bunch of frames 50 milliseconds apart
- for (frameTimeMillis in 0..1000 step interval) {
- clock.frame(frameTimeMillis * 1_000_000L)
- delay(5)
- }
- }
- launch {
- // The first frame should start at 100ms
- var playTimeMillis = 0L
- animatable.animateTo(to, animationSpec = tween(500)) {
- assertTrue("PlayTime Millis: $playTimeMillis", isRunning)
- assertEquals(to, targetValue)
- val expectedValue = anim.getValueFromMillis(playTimeMillis)
- assertEquals(
- "PlayTime Millis: $playTimeMillis",
- expectedValue.x,
- value.x,
- 0.001f
- )
- assertEquals(
- "PlayTime Millis: $playTimeMillis",
- expectedValue.y,
- value.y,
- 0.001f
- )
- playTimeMillis += interval
-
- if (playTimeMillis == 300L) {
- // Prematurely cancel the animation and check corresponding states
- [email protected] {
- stop()
- assertFalse(isRunning)
- assertEquals(playTimeMillis, 300L)
- assertEquals(to, animatable.targetValue)
- assertEquals(AnimationVector(0f, 0f), animatable.velocityVector)
- }
- }
- }
- }
- }
- }
- }
-
- @Test
- fun animateToWithInterruption() {
- runBlocking {
- val anim1 =
- TargetBasedAnimation(
- tween(200, easing = LinearEasing),
- Float.VectorConverter,
- 0f,
- 200f
- )
- val clock = MyTestFrameClock()
- val interval = 50
- coroutineScope {
- withContext(clock) {
- val animatable = Animatable(0f)
- var playTimeMillis by mutableStateOf(0L)
-
- suspend fun createInterruption() {
- val anim2 =
- TargetBasedAnimation(
- spring(),
- Float.VectorConverter,
- animatable.value,
- 300f,
- animatable.velocity
- )
- assertEquals(100L, playTimeMillis)
- var firstFrame = true
- val result2 =
- animatable.animateTo(300f, spring()) {
- // First frame will arrive with a timestamp of the time of
- // interruption,
- // which is 100ms. The subsequent frames will be consistent with
- // what's
- // tracked in `playTimeMillis`.
- val playTime = if (firstFrame) 100L else playTimeMillis
- assertTrue(isRunning)
- assertEquals(300f, targetValue)
- assertEquals(anim2.getValueFromMillis((playTime - 100)), value)
- assertEquals(
- anim2.getVelocityFromMillis((playTime - 100)),
- velocity
- )
- if (!firstFrame) {
- playTimeMillis += interval
- clock.trySendFrame(playTimeMillis * 1_000_000L)
- } else {
- firstFrame = false
- }
- }
- assertFalse(animatable.isRunning)
- assertEquals(AnimationEndReason.Finished, result2.endReason)
- assertEquals(300f, animatable.targetValue)
- assertEquals(300f, animatable.value)
- assertEquals(0f, animatable.velocity)
- }
-
- clock.trySendFrame(0)
- launch {
- try {
- animatable.animateTo(
- 200f,
- animationSpec = tween(200, easing = LinearEasing)
- ) {
- assertTrue(isRunning)
- assertEquals(targetValue, 200f)
- assertEquals(anim1.getValueFromMillis(playTimeMillis), value)
- assertEquals(anim1.getVelocityFromMillis(playTimeMillis), velocity)
-
- assertTrue(playTimeMillis <= 100)
- if (playTimeMillis == 100L) {
- [email protected] {
- // No more new frame until the ongoing animation is
- // canceled.
- createInterruption()
- }
- } else {
- playTimeMillis += interval
- clock.trySendFrame(playTimeMillis * 1_000_000L)
- }
- }
- } finally {
- // At this point the previous animation on the Animatable has been
- // canceled. Pump a frame to get the new animation going.
- playTimeMillis += interval
- clock.trySendFrame(playTimeMillis * 1_000_000L)
- }
- }
- }
- }
- }
- }
-
- @Test
- fun testUpdateBounds() {
- val animatable = Animatable(5f)
- // Update bounds when *not* running
- animatable.updateBounds(0f, 4f)
- assertEquals(4f, animatable.value)
- runBlocking {
- val clock = SuspendAnimationTest.TestFrameClock()
- // Put two frames in clock
- clock.frame(0L)
- clock.frame(200 * 1_000_000L)
-
- withContext(clock) {
- animatable.animateTo(4f, tween(100)) {
- if (animatable.upperBound == 4f) {
- // Update bounds while running
- animatable.updateBounds(-4f, 0f)
- }
- }
- }
- }
- assertEquals(0f, animatable.value)
-
- // Snap to value out of bounds
- runBlocking { animatable.snapTo(animatable.lowerBound!! - 100f) }
- assertEquals(animatable.lowerBound!!, animatable.value)
- }
-
- @Test
- fun testIntSize_alwaysWithinValidBounds() {
- val animatable =
- Animatable(
- initialValue = IntSize(10, 10),
- typeConverter = IntSize.VectorConverter,
- visibilityThreshold = IntSize.VisibilityThreshold
- )
-
- val values = mutableListOf<IntSize>()
-
- runBlocking {
- val clock = SuspendAnimationTest.TestFrameClock()
-
- // Add frames to evaluate at
- clock.frame(0L)
- clock.frame(25L * 1_000_000L)
- clock.frame(75L * 1_000_000L)
- clock.frame(100L * 1_000_000L)
-
- withContext(clock) {
- // Animate linearly from -100 to 100
- animatable.animateTo(
- IntSize(100, 100),
- keyframes {
- durationMillis = 100
- IntSize(-100, -100) at 0 using LinearEasing
- }
- ) {
- values.add(value)
- }
- }
- }
-
- // The internal animation is expected to be: -100, -50, 50, 100. But for IntSize, we don't
- // support negative values, so it's clamped to Zero
- assertEquals(4, values.size)
- assertEquals(IntSize.Zero, values[0])
- assertEquals(IntSize.Zero, values[1])
- assertEquals(IntSize(50, 50), values[2])
- assertEquals(IntSize(100, 100), values[3])
- }
-
- @Test
- fun animationResult_toString() {
- val animatable =
- AnimationResult(endReason = AnimationEndReason.Finished, endState = AnimationState(42f))
- val string = animatable.toString()
- assertThat(string).contains(AnimationResult::class.java.simpleName)
- assertThat(string).contains("endReason=Finished")
- assertThat(string).contains("endState=")
- }
-
- @Test
- fun animationState_toString() {
- val state =
- AnimationState(
- initialValue = 42f,
- initialVelocity = 2f,
- lastFrameTimeNanos = 4000L,
- finishedTimeNanos = 3000L,
- isRunning = true
- )
- val string = state.toString()
- assertThat(string).contains(AnimationState::class.java.simpleName)
- assertThat(string).contains("value=42.0")
- assertThat(string).contains("velocity=2.0")
- assertThat(string).contains("lastFrameTimeNanos=4000")
- assertThat(string).contains("finishedTimeNanos=3000")
- assertThat(string).contains("isRunning=true")
- }
-
- private class MyTestFrameClock : MonotonicFrameClock {
- // Make the send non-blocking
- private val frameCh = Channel<Long>(Channel.UNLIMITED)
-
- suspend fun frame(frameTimeNanos: Long) {
- frameCh.send(frameTimeNanos)
- }
-
- fun trySendFrame(frameTimeNanos: Long) {
- frameCh.trySend(frameTimeNanos)
- }
-
- override suspend fun <R> withFrameNanos(onFrame: (Long) -> R): R =
- onFrame(frameCh.receive())
- }
-}
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/EasingTest.android.kt b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/EasingTest.android.kt
new file mode 100644
index 0000000..4b5a229
--- /dev/null
+++ b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/EasingTest.android.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.animation.core
+
+import androidx.compose.ui.util.floatFromBits
+import kotlin.math.ulp
+import kotlin.test.Test
+import kotlin.test.assertTrue
+
+// This test can't be in commonTest because
+// Float.ulp is jvm only: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.math/ulp.html
+class EasingTestAndroid {
+ private val ZeroEpsilon = -(1.0f.ulp * 2.0f)
+ private val OneEpsilon = 1.0f + 1.0f.ulp * 2.0f
+
+ @Test
+ fun canSolveCubicForFractionsCloseToOne() {
+ // Only test curves defined in [0..1]
+ // For instance, EaseInOutBack is defined in a larger domain, so exclude it from the list
+ val curves =
+ listOf(
+ CubicBezierEasing(0.4f, 0.0f, 0.2f, 1.0f),
+ Ease,
+ EaseIn,
+ EaseInBack,
+ EaseInCirc,
+ EaseInCubic,
+ EaseInExpo,
+ EaseInOut,
+ EaseInOutCirc,
+ EaseInOutCubic,
+ EaseInOutExpo,
+ EaseInOutQuad,
+ EaseInOutQuart,
+ EaseInOutQuint,
+ EaseInOutSine,
+ EaseInOutQuad,
+ EaseInOutQuart,
+ EaseInOutQuint,
+ EaseInSine,
+ EaseOut,
+ EaseOutCirc,
+ EaseOutCubic,
+ EaseOutExpo,
+ EaseOutQuad,
+ EaseOutQuart,
+ EaseOutQuint,
+ EaseOutSine
+ )
+
+ for (curve in curves) {
+ // Test the last 16 ulps until 1.0f
+ for (i in 0x3f7ffff0..0x3f7fffff) {
+ val fraction = floatFromBits(i)
+ val t = curve.transform(fraction)
+ assertTrue(
+ t in -ZeroEpsilon..OneEpsilon,
+ "f($fraction) = $t out of range for $curve | ${-ZeroEpsilon}..${OneEpsilon}"
+ )
+ }
+ }
+ }
+}
diff --git a/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/AnimatableTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/AnimatableTest.kt
new file mode 100644
index 0000000..ea43d32
--- /dev/null
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/AnimatableTest.kt
@@ -0,0 +1,361 @@
+/*
+ * 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.animation.core
+
+import androidx.compose.runtime.MonotonicFrameClock
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.unit.IntSize
+import androidx.kruth.assertThat
+import kotlin.math.abs
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.withContext
+
+class AnimatableTest {
+ @Test
+ fun animateDecayTest() = runTest {
+ val from = 9f
+ val initialVelocity = 20f
+ val decaySpec = FloatExponentialDecaySpec()
+ val anim = DecayAnimation(decaySpec, initialValue = from, initialVelocity = initialVelocity)
+ val clock = SuspendAnimationTest.TestFrameClock()
+ val interval = 50
+ withContext(clock) {
+ // Put in a bunch of frames 50 milliseconds apart
+ for (frameTimeMillis in 0..5000 step interval) {
+ clock.frame(frameTimeMillis * 1_000_000L)
+ }
+ var playTimeMillis = 0L
+ val animatable = Animatable(9f)
+ val result =
+ animatable.animateDecay(20f, animationSpec = exponentialDecay()) {
+ assertTrue(isRunning)
+ assertEquals(anim.targetValue, targetValue)
+ assertEquals(anim.getValueFromMillis(playTimeMillis), value, 0.001f)
+ assertEquals(anim.getVelocityFromMillis(playTimeMillis), velocity, 0.001f)
+ playTimeMillis += interval
+ assertEquals(value, animatable.value, 0.0001f)
+ assertEquals(velocity, animatable.velocity, 0.0001f)
+ }
+ // After animation
+ assertEquals(anim.targetValue, animatable.value)
+ assertEquals(false, animatable.isRunning)
+ assertEquals(0f, animatable.velocity)
+ assertEquals(AnimationEndReason.Finished, result.endReason)
+ assertTrue(abs(result.endState.velocity) <= decaySpec.absVelocityThreshold)
+ }
+ }
+
+ @Test
+ fun animateToTest() = runTest {
+ val anim =
+ TargetBasedAnimation(
+ spring(dampingRatio = Spring.DampingRatioMediumBouncy),
+ Float.VectorConverter,
+ initialValue = 0f,
+ targetValue = 1f
+ )
+ val clock = SuspendAnimationTest.TestFrameClock()
+ val interval = 50
+ val animatable = Animatable(0f)
+ withContext(clock) {
+ // Put in a bunch of frames 50 milliseconds apart
+ for (frameTimeMillis in 0..5000 step interval) {
+ clock.frame(frameTimeMillis * 1_000_000L)
+ }
+ var playTimeMillis = 0L
+ val result =
+ animatable.animateTo(1f, spring(dampingRatio = Spring.DampingRatioMediumBouncy)) {
+ assertTrue(isRunning)
+ assertEquals(1f, targetValue)
+ assertEquals(anim.getValueFromMillis(playTimeMillis), value, 0.001f)
+ assertEquals(anim.getVelocityFromMillis(playTimeMillis), velocity, 0.001f)
+ playTimeMillis += interval
+ }
+ // After animation
+ assertEquals(anim.targetValue, animatable.value)
+ assertEquals(0f, animatable.velocity)
+ assertEquals(false, animatable.isRunning)
+ assertEquals(AnimationEndReason.Finished, result.endReason)
+ }
+ }
+
+ @Test
+ fun animateToGenericTypeTest() = runTest {
+ val from = Offset(666f, 321f)
+ val to = Offset(919f, 864f)
+ val offsetToVector: TwoWayConverter<Offset, AnimationVector2D> =
+ TwoWayConverter(
+ convertToVector = { AnimationVector2D(it.x, it.y) },
+ convertFromVector = { Offset(it.v1, it.v2) }
+ )
+ val anim =
+ TargetBasedAnimation(tween(500), offsetToVector, initialValue = from, targetValue = to)
+ val clock = SuspendAnimationTest.TestFrameClock()
+ val interval = 50
+ val animatable = Animatable(initialValue = from, typeConverter = offsetToVector)
+ coroutineScope {
+ withContext(clock) {
+ launch {
+ // Put in a bunch of frames 50 milliseconds apart
+ for (frameTimeMillis in 0..1000 step interval) {
+ clock.frame(frameTimeMillis * 1_000_000L)
+ delay(5)
+ }
+ }
+ launch {
+ // The first frame should start at 100ms
+ var playTimeMillis = 0L
+ animatable.animateTo(to, animationSpec = tween(500)) {
+ assertTrue(isRunning, "PlayTime Millis: $playTimeMillis")
+ assertEquals(to, targetValue)
+ val expectedValue = anim.getValueFromMillis(playTimeMillis)
+ assertEquals(
+ expectedValue.x,
+ value.x,
+ 0.001f,
+ "PlayTime Millis: $playTimeMillis"
+ )
+ assertEquals(
+ expectedValue.y,
+ value.y,
+ 0.001f,
+ "PlayTime Millis: $playTimeMillis"
+ )
+ playTimeMillis += interval
+
+ if (playTimeMillis == 300L) {
+ // Prematurely cancel the animation and check corresponding states
+ [email protected] {
+ stop()
+ assertFalse(isRunning)
+ assertEquals(playTimeMillis, 300L)
+ assertEquals(to, animatable.targetValue)
+ assertEquals(AnimationVector(0f, 0f), animatable.velocityVector)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @Test
+ fun animateToWithInterruption() = runTest {
+ val anim1 =
+ TargetBasedAnimation(tween(200, easing = LinearEasing), Float.VectorConverter, 0f, 200f)
+ val clock = MyTestFrameClock()
+ val interval = 50
+ coroutineScope {
+ withContext(clock) {
+ val animatable = Animatable(0f)
+ var playTimeMillis by mutableStateOf(0L)
+
+ suspend fun createInterruption() {
+ val anim2 =
+ TargetBasedAnimation(
+ spring(),
+ Float.VectorConverter,
+ animatable.value,
+ 300f,
+ animatable.velocity
+ )
+ assertEquals(100L, playTimeMillis)
+ var firstFrame = true
+ val result2 =
+ animatable.animateTo(300f, spring()) {
+ // First frame will arrive with a timestamp of the time of
+ // interruption,
+ // which is 100ms. The subsequent frames will be consistent with
+ // what's
+ // tracked in `playTimeMillis`.
+ val playTime = if (firstFrame) 100L else playTimeMillis
+ assertTrue(isRunning)
+ assertEquals(300f, targetValue)
+ assertEquals(anim2.getValueFromMillis((playTime - 100)), value)
+ assertEquals(anim2.getVelocityFromMillis((playTime - 100)), velocity)
+ if (!firstFrame) {
+ playTimeMillis += interval
+ clock.trySendFrame(playTimeMillis * 1_000_000L)
+ } else {
+ firstFrame = false
+ }
+ }
+ assertFalse(animatable.isRunning)
+ assertEquals(AnimationEndReason.Finished, result2.endReason)
+ assertEquals(300f, animatable.targetValue)
+ assertEquals(300f, animatable.value)
+ assertEquals(0f, animatable.velocity)
+ }
+
+ clock.trySendFrame(0)
+ launch {
+ try {
+ animatable.animateTo(
+ 200f,
+ animationSpec = tween(200, easing = LinearEasing)
+ ) {
+ assertTrue(isRunning)
+ assertEquals(targetValue, 200f)
+ assertEquals(anim1.getValueFromMillis(playTimeMillis), value)
+ assertEquals(anim1.getVelocityFromMillis(playTimeMillis), velocity)
+
+ assertTrue(playTimeMillis <= 100)
+ if (playTimeMillis == 100L) {
+ [email protected] {
+ // No more new frame until the ongoing animation is
+ // canceled.
+ createInterruption()
+ }
+ } else {
+ playTimeMillis += interval
+ clock.trySendFrame(playTimeMillis * 1_000_000L)
+ }
+ }
+ } finally {
+ // At this point the previous animation on the Animatable has been
+ // canceled. Pump a frame to get the new animation going.
+ playTimeMillis += interval
+ clock.trySendFrame(playTimeMillis * 1_000_000L)
+ }
+ }
+ }
+ }
+ }
+
+ @Test
+ fun testUpdateBounds() = runTest {
+ val animatable = Animatable(5f)
+ // Update bounds when *not* running
+ animatable.updateBounds(0f, 4f)
+ assertEquals(4f, animatable.value)
+ val clock = SuspendAnimationTest.TestFrameClock()
+ // Put two frames in clock
+ clock.frame(0L)
+ clock.frame(200 * 1_000_000L)
+
+ withContext(clock) {
+ animatable.animateTo(4f, tween(100)) {
+ if (animatable.upperBound == 4f) {
+ // Update bounds while running
+ animatable.updateBounds(-4f, 0f)
+ }
+ }
+ }
+ assertEquals(0f, animatable.value)
+
+ // Snap to value out of bounds
+ animatable.snapTo(animatable.lowerBound!! - 100f)
+ assertEquals(animatable.lowerBound!!, animatable.value)
+ }
+
+ @Test
+ fun testIntSize_alwaysWithinValidBounds() = runTest {
+ val animatable =
+ Animatable(
+ initialValue = IntSize(10, 10),
+ typeConverter = IntSize.VectorConverter,
+ visibilityThreshold = IntSize.VisibilityThreshold
+ )
+
+ val values = mutableListOf<IntSize>()
+
+ val clock = SuspendAnimationTest.TestFrameClock()
+
+ // Add frames to evaluate at
+ clock.frame(0L)
+ clock.frame(25L * 1_000_000L)
+ clock.frame(75L * 1_000_000L)
+ clock.frame(100L * 1_000_000L)
+
+ withContext(clock) {
+ // Animate linearly from -100 to 100
+ animatable.animateTo(
+ IntSize(100, 100),
+ keyframes {
+ durationMillis = 100
+ IntSize(-100, -100) at 0 using LinearEasing
+ }
+ ) {
+ values.add(value)
+ }
+ }
+
+ // The internal animation is expected to be: -100, -50, 50, 100. But for IntSize, we don't
+ // support negative values, so it's clamped to Zero
+ assertEquals(4, values.size)
+ assertEquals(IntSize.Zero, values[0])
+ assertEquals(IntSize.Zero, values[1])
+ assertEquals(IntSize(50, 50), values[2])
+ assertEquals(IntSize(100, 100), values[3])
+ }
+
+ @Test
+ fun animationResult_toString() {
+ val animatable =
+ AnimationResult(endReason = AnimationEndReason.Finished, endState = AnimationState(42f))
+ val string = animatable.toString()
+ assertThat(string).contains(AnimationResult::class.simpleName!!)
+ assertThat(string).contains("endReason=Finished")
+ assertThat(string).contains("endState=")
+ }
+
+ @Test
+ fun animationState_toString() {
+ val state =
+ AnimationState(
+ initialValue = 42f,
+ initialVelocity = 2f,
+ lastFrameTimeNanos = 4000L,
+ finishedTimeNanos = 3000L,
+ isRunning = true
+ )
+ val string = state.toString()
+ assertThat(string).contains(AnimationState::class.simpleName!!)
+ assertThat(string).contains("value=42.0")
+ assertThat(string).contains("velocity=2.0")
+ assertThat(string).contains("lastFrameTimeNanos=4000")
+ assertThat(string).contains("finishedTimeNanos=3000")
+ assertThat(string).contains("isRunning=true")
+ }
+
+ private class MyTestFrameClock : MonotonicFrameClock {
+ // Make the send non-blocking
+ private val frameCh = Channel<Long>(Channel.UNLIMITED)
+
+ suspend fun frame(frameTimeNanos: Long) {
+ frameCh.send(frameTimeNanos)
+ }
+
+ fun trySendFrame(frameTimeNanos: Long) {
+ frameCh.trySend(frameTimeNanos)
+ }
+
+ override suspend fun <R> withFrameNanos(onFrame: (Long) -> R): R =
+ onFrame(frameCh.receive())
+ }
+}
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/AnimationTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/AnimationTest.kt
similarity index 98%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/AnimationTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/AnimationTest.kt
index b09fcfd..e935a2c 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/AnimationTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/AnimationTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 The Android Open Source Project
+ * 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.
@@ -16,13 +16,10 @@
package androidx.compose.animation.core
-import java.lang.Long.max
-import org.junit.Assert.assertEquals
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import kotlin.math.max
+import kotlin.test.Test
+import kotlin.test.assertEquals
-@RunWith(JUnit4::class)
class AnimationTest {
@Test
fun testSnap() {
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/AnimationTestUtils.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/AnimationTestUtils.kt
similarity index 98%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/AnimationTestUtils.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/AnimationTestUtils.kt
index 6cfd113..286efb5 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/AnimationTestUtils.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/AnimationTestUtils.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 The Android Open Source Project
+ * 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.
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/AnimationVectorTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/AnimationVectorTest.kt
similarity index 91%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/AnimationVectorTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/AnimationVectorTest.kt
index 263ac7b..f0834b0 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/AnimationVectorTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/AnimationVectorTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 The Android Open Source Project
+ * 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.
@@ -16,13 +16,10 @@
package androidx.compose.animation.core
-import junit.framework.TestCase.assertEquals
-import org.junit.Assert.assertNotEquals
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
-@RunWith(JUnit4::class)
class AnimationVectorTest {
@Test
fun testReset() {
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/ArcAnimationTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/ArcAnimationTest.kt
similarity index 98%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/ArcAnimationTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/ArcAnimationTest.kt
index a8a8e70..957eafb 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/ArcAnimationTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/ArcAnimationTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 The Android Open Source Project
+ * 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.
@@ -19,17 +19,14 @@
import androidx.compose.animation.core.ArcMode.Companion.ArcAbove
import androidx.compose.animation.core.ArcMode.Companion.ArcBelow
import androidx.compose.animation.core.ArcMode.Companion.ArcLinear
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertNotEquals
-import org.junit.Assert.assertTrue
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import kotlin.test.assertTrue
/** Mostly tests some mathematical assumptions about arcs. */
@Suppress("JoinDeclarationAndAssignment") // Looks kinda messy
@OptIn(ExperimentalAnimationSpecApi::class)
-@RunWith(JUnit4::class)
class ArcAnimationTest {
// Animation parameters used in all tests
private val timeMillis = 1000
@@ -512,7 +509,7 @@
start = endTime * segment.startPercent,
end = endTime * segment.endPercent
)
- assertEquals("Graph on X dimension not equals", expectGraphX, arcSplineX)
+ assertEquals(expectGraphX, arcSplineX, message = "Graph on X dimension not equals")
val arcSplineY =
plot2DArcSpline(
@@ -521,7 +518,7 @@
start = endTime * segment.startPercent,
end = endTime * segment.endPercent
)
- assertEquals("Graph on Y dimension not equals", expectGraphY, arcSplineY)
+ assertEquals(expectGraphY, arcSplineY, message = "Graph on Y dimension not equals")
}
private inline fun <reified V : AnimationVector> VectorizedDurationBasedAnimationSpec<V>
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/DecayAnimationTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/DecayAnimationTest.kt
similarity index 91%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/DecayAnimationTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/DecayAnimationTest.kt
index da144e4f..092be60 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/DecayAnimationTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/DecayAnimationTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 The Android Open Source Project
+ * 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.
@@ -16,15 +16,13 @@
package androidx.compose.animation.core
-import junit.framework.TestCase.assertEquals
-import junit.framework.TestCase.assertTrue
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import kotlin.math.abs
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
const val epsilon = 0.00001f
-@RunWith(JUnit4::class)
class DecayAnimationTest {
@Test
@@ -47,7 +45,7 @@
if (!finished) {
// Before the animation finishes, absolute velocity is above the threshold
- assertTrue(Math.abs(velocity) >= 2.0f)
+ assertTrue(abs(velocity) >= 2.0f)
assertEquals(value, animWrapper.getValueFromNanos(playTimeNanos), epsilon)
assertEquals(
velocity,
@@ -57,7 +55,7 @@
assertTrue(playTimeNanos < finishTimeNanos)
} else {
// When the animation is finished, expect absolute velocity < threshold
- assertTrue(Math.abs(velocity) < 2.0f)
+ assertTrue(abs(velocity) < 2.0f)
// Once the animation is finished, the value should not change any more
assertEquals(finishValue, animWrapper.getValueFromNanos(playTimeNanos), epsilon)
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/DelayedAnimationTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/DelayedAnimationTest.kt
similarity index 97%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/DelayedAnimationTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/DelayedAnimationTest.kt
index 97feb08..52f232c 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/DelayedAnimationTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/DelayedAnimationTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 The Android Open Source Project
+ * 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.
@@ -16,12 +16,9 @@
package androidx.compose.animation.core
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import androidx.kruth.assertThat
+import kotlin.test.Test
-@RunWith(JUnit4::class)
class DelayedAnimationTest {
@Test
fun duration() {
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/DurationScaleTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/DurationScaleTest.kt
similarity index 87%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/DurationScaleTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/DurationScaleTest.kt
index 8e87eb0..70c84dd 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/DurationScaleTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/DurationScaleTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 The Android Open Source Project
+ * 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.
@@ -17,18 +17,15 @@
package androidx.compose.animation.core
import androidx.compose.ui.MotionDurationScale
+import kotlin.test.Test
+import kotlin.test.assertEquals
import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
-import org.junit.Assert.assertEquals
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-@RunWith(JUnit4::class)
class DurationScaleTest {
@Test
- fun testAnimatable() = runBlocking {
+ fun testAnimatable() = runTest {
val clock = SuspendAnimationTest.TestFrameClock()
withContext(
clock +
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/EasingTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/EasingUnitTest.kt
similarity index 62%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/EasingTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/EasingUnitTest.kt
index 816bec4..c5e5ca3 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/EasingTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/EasingUnitTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 The Android Open Source Project
+ * 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.
@@ -17,19 +17,13 @@
package androidx.compose.animation.core
import androidx.compose.ui.util.floatFromBits
-import com.google.common.truth.Truth.assertThat
-import junit.framework.TestCase.assertEquals
-import junit.framework.TestCase.assertTrue
-import kotlin.math.ulp
-import org.junit.Assert.assertNotEquals
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import androidx.kruth.assertThat
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import kotlin.test.assertTrue
-@RunWith(JUnit4::class)
-class EasingTest {
- private val ZeroEpsilon = -(1.0f.ulp * 2.0f)
- private val OneEpsilon = 1.0f + 1.0f.ulp * 2.0f
+class EasingUnitTest {
@Test
fun cubicBezierStartsAt0() {
@@ -105,52 +99,4 @@
assertTrue(t in 0.0f..1.0f)
}
}
-
- @Test
- fun canSolveCubicForFractionsCloseToOne() {
- // Only test curves defined in [0..1]
- // For instance, EaseInOutBack is defined in a larger domain, so exclude it from the list
- val curves =
- listOf(
- CubicBezierEasing(0.4f, 0.0f, 0.2f, 1.0f),
- Ease,
- EaseIn,
- EaseInBack,
- EaseInCirc,
- EaseInCubic,
- EaseInExpo,
- EaseInOut,
- EaseInOutCirc,
- EaseInOutCubic,
- EaseInOutExpo,
- EaseInOutQuad,
- EaseInOutQuart,
- EaseInOutQuint,
- EaseInOutSine,
- EaseInOutQuad,
- EaseInOutQuart,
- EaseInOutQuint,
- EaseInSine,
- EaseOut,
- EaseOutCirc,
- EaseOutCubic,
- EaseOutExpo,
- EaseOutQuad,
- EaseOutQuart,
- EaseOutQuint,
- EaseOutSine
- )
-
- for (curve in curves) {
- // Test the last 16 ulps until 1.0f
- for (i in 0x3f7ffff0..0x3f7fffff) {
- val fraction = floatFromBits(i)
- val t = curve.transform(fraction)
- assertTrue(
- "f($fraction) = $t out of range for $curve",
- t in -ZeroEpsilon..OneEpsilon
- )
- }
- }
- }
}
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/IsInfiniteTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/IsInfiniteTest.kt
similarity index 93%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/IsInfiniteTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/IsInfiniteTest.kt
index f4a65b4..4c11a00 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/IsInfiniteTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/IsInfiniteTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 The Android Open Source Project
+ * 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.
@@ -16,12 +16,9 @@
package androidx.compose.animation.core
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import androidx.kruth.assertThat
+import kotlin.test.Test
-@RunWith(JUnit4::class)
class IsInfiniteTest {
@Test
fun testTweenIsFinite() {
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeAnimationTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/KeyframeAnimationTest.kt
similarity index 91%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeAnimationTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/KeyframeAnimationTest.kt
index ad48e92..ca6754d 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeAnimationTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/KeyframeAnimationTest.kt
@@ -1,21 +1,5 @@
/*
- * Copyright 2019 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.
- */
-
-/*
- * Copyright 2019 The Android Open Source Project
+ * 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.
@@ -32,14 +16,11 @@
package androidx.compose.animation.core
-import com.google.common.truth.Truth.assertThat
-import junit.framework.TestCase.assertEquals
-import junit.framework.TestCase.assertTrue
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import androidx.kruth.assertThat
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
-@RunWith(JUnit4::class)
class KeyframeAnimationTest {
@Test
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeArcAnimationTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/KeyframeArcAnimationTest.kt
similarity index 95%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeArcAnimationTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/KeyframeArcAnimationTest.kt
index 97b2ea2..d4ebe2a 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeArcAnimationTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/KeyframeArcAnimationTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 The Android Open Source Project
+ * 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.
@@ -20,15 +20,12 @@
import androidx.compose.animation.core.ArcMode.Companion.ArcBelow
import androidx.compose.animation.core.ArcMode.Companion.ArcLinear
import androidx.compose.ui.geometry.Offset
-import junit.framework.TestCase.assertEquals
-import org.junit.Assert.assertTrue
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
@Suppress("JoinDeclarationAndAssignment") // Looks kinda messy
@OptIn(ExperimentalAnimationSpecApi::class)
-@RunWith(JUnit4::class)
class KeyframeArcAnimationTest {
private val timeMillis = 3000
private val initialValue = 0f
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeSplineAnimationTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/KeyframeSplineAnimationTest.kt
similarity index 97%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeSplineAnimationTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/KeyframeSplineAnimationTest.kt
index fcae260..31acd22 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeSplineAnimationTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/KeyframeSplineAnimationTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 The Android Open Source Project
+ * 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.
@@ -17,15 +17,12 @@
package androidx.compose.animation.core
import androidx.compose.ui.geometry.Offset
-import junit.framework.TestCase.assertEquals
-import org.junit.Assert.assertNotEquals
-import org.junit.Assert.assertTrue
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import kotlin.test.assertTrue
@OptIn(ExperimentalAnimationSpecApi::class)
-@RunWith(JUnit4::class)
class KeyframeSplineAnimationTest {
/** See [MonoSplineTest] to test the interpolation curves. */
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/MonoSplineTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/MonoSplineTest.kt
similarity index 90%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/MonoSplineTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/MonoSplineTest.kt
index dd3d23d..3b514b1 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/MonoSplineTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/MonoSplineTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 The Android Open Source Project
+ * 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.
@@ -16,13 +16,11 @@
package androidx.compose.animation.core
-import java.util.Arrays
-import org.junit.Assert.assertEquals
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import kotlin.math.max
+import kotlin.math.min
+import kotlin.test.Test
+import kotlin.test.assertEquals
-@RunWith(JUnit4::class)
class MonoSplineTest {
@Test
fun testCurveFit01() {
@@ -108,14 +106,14 @@
var maxY = y[0]
var ret = ""
for (i in x.indices) {
- minX = Math.min(minX, x[i])
- maxX = Math.max(maxX, x[i])
- minY = Math.min(minY, y[i])
- maxY = Math.max(maxY, y[i])
+ minX = min(minX, x[i])
+ maxX = max(maxX, x[i])
+ minY = min(minY, y[i])
+ maxY = max(maxY, y[i])
}
val c = Array(dimy) { CharArray(dimx) }
for (i in 0 until dimy) {
- Arrays.fill(c[i], ' ')
+ repeat(c[i].size) { c[i][it] = ' ' }
}
val dimx1 = dimx - 1
val dimy1 = dimy - 1
@@ -134,14 +132,14 @@
v = (v * 1000 + 0.5).toInt() / 1000f
ret +=
if (i % 5 == 0 || i == c.size - 1) {
- "|" + String(c[i]) + "| " + v + "\n"
+ "|" + c[i].concatToString() + "| " + v + "\n"
} else {
- "|" + String(c[i]) + "|\n"
+ "|" + c[i].concatToString() + "|\n"
}
}
val minStr = ((minX * 1000 + 0.5).toInt() / 1000f).toString()
val maxStr = ((maxX * 1000 + 0.5).toInt() / 1000f).toString()
- var s = minStr + String(CharArray(dimx) { ' ' })
+ var s = minStr + CharArray(dimx) { ' ' }.concatToString()
s = s.substring(0, dimx - maxStr.length + 2) + maxStr + '\n'
return (ret + s).trimIndent()
}
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/PhysicsAnimationTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/PhysicsAnimationTest.kt
similarity index 97%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/PhysicsAnimationTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/PhysicsAnimationTest.kt
index 6adbe32..3ac7408 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/PhysicsAnimationTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/PhysicsAnimationTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 The Android Open Source Project
+ * 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.
@@ -16,15 +16,12 @@
package androidx.compose.animation.core
-import com.google.common.truth.Truth.assertThat
-import junit.framework.TestCase.assertEquals
+import androidx.kruth.assertThat
import kotlin.math.sign
-import org.junit.Assert.assertTrue
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
-@RunWith(JUnit4::class)
class PhysicsAnimationTest {
@Test
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/RepeatableAnimationTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/RepeatableAnimationTest.kt
similarity index 96%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/RepeatableAnimationTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/RepeatableAnimationTest.kt
index 4f29357..13ddd1c 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/RepeatableAnimationTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/RepeatableAnimationTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 The Android Open Source Project
+ * 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.
@@ -16,14 +16,11 @@
package androidx.compose.animation.core
-import com.google.common.truth.Truth.assertThat
-import junit.framework.TestCase.assertEquals
-import junit.framework.TestCase.assertFalse
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import androidx.kruth.assertThat
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
-@RunWith(JUnit4::class)
class RepeatableAnimationTest {
private val DelayedAnimation =
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/SnapAnimationTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/SnapAnimationTest.kt
similarity index 82%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/SnapAnimationTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/SnapAnimationTest.kt
index 3ba3606..ce0efae 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/SnapAnimationTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/SnapAnimationTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 The Android Open Source Project
+ * 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.
@@ -16,12 +16,9 @@
package androidx.compose.animation.core
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import androidx.kruth.assertThat
+import kotlin.test.Test
-@RunWith(JUnit4::class)
class SnapAnimationTest {
@Test
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/SuspendAnimationTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/SuspendAnimationTest.kt
similarity index 93%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/SuspendAnimationTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/SuspendAnimationTest.kt
index b038c33..0746cf5 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/SuspendAnimationTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/SuspendAnimationTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 The Android Open Source Project
+ * 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.
@@ -18,20 +18,17 @@
import androidx.compose.runtime.MonotonicFrameClock
import androidx.compose.ui.geometry.Offset
-import junit.framework.TestCase.assertEquals
-import junit.framework.TestCase.assertFalse
-import junit.framework.TestCase.assertTrue
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-@RunWith(JUnit4::class)
class SuspendAnimationTest {
@Test
- fun animateFloatVariantTest() = runBlocking {
+ fun animateFloatVariantTest() = runTest {
val anim =
TargetBasedAnimation(
spring(dampingRatio = Spring.DampingRatioMediumBouncy),
@@ -58,7 +55,7 @@
}
@Test
- fun animateGenericsVariantTest() = runBlocking {
+ fun animateGenericsVariantTest() = runTest {
val from = Offset(666f, 321f)
val to = Offset(919f, 864f)
val offsetToVector: TwoWayConverter<Offset, AnimationVector2D> =
@@ -86,7 +83,7 @@
}
@Test
- fun animateDecayTest() = runBlocking {
+ fun animateDecayTest() = runTest {
val from = 666f
val velocity = 999f
val anim =
@@ -115,7 +112,7 @@
@Test
fun animateToTest() {
- runBlocking {
+ runTest {
val from = Offset(666f, 321f)
val to = Offset(919f, 864f)
val offsetToVector: TwoWayConverter<Offset, AnimationVector2D> =
@@ -179,7 +176,7 @@
}
@Test
- fun animateDecayOnAnimationStateTest() = runBlocking {
+ fun animateDecayOnAnimationStateTest() = runTest {
val from = 9f
val initialVelocity = 20f
val anim =
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/TweenAnimationTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/TweenAnimationTest.kt
similarity index 91%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/TweenAnimationTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/TweenAnimationTest.kt
index 42c9d1e..ed55738 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/TweenAnimationTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/TweenAnimationTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 The Android Open Source Project
+ * 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.
@@ -16,12 +16,9 @@
package androidx.compose.animation.core
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import androidx.kruth.assertThat
+import kotlin.test.Test
-@RunWith(JUnit4::class)
class TweenAnimationTest {
@Test
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/TypeConverterTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/TypeConverterTest.kt
similarity index 84%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/TypeConverterTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/TypeConverterTest.kt
index 81b503b..db2f485 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/TypeConverterTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/TypeConverterTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 The Android Open Source Project
+ * 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.
@@ -16,12 +16,9 @@
package androidx.compose.animation.core
-import org.junit.Assert.assertEquals
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import kotlin.test.Test
+import kotlin.test.assertEquals
-@RunWith(JUnit4::class)
class TypeConverterTest {
@Test
fun testFloatToVectorConverter() {
diff --git a/compose/animation/animation/build.gradle b/compose/animation/animation/build.gradle
index f968628..c7595d2 100644
--- a/compose/animation/animation/build.gradle
+++ b/compose/animation/animation/build.gradle
@@ -127,10 +127,6 @@
samples(project(":compose:animation:animation:animation-samples"))
}
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
-}
-
android {
compileSdk 35
namespace "androidx.compose.animation"
diff --git a/compose/animation/animation/integration-tests/animation-demos/build.gradle b/compose/animation/animation/integration-tests/animation-demos/build.gradle
index 49c87a5..995c88c 100644
--- a/compose/animation/animation/integration-tests/animation-demos/build.gradle
+++ b/compose/animation/animation/integration-tests/animation-demos/build.gradle
@@ -36,10 +36,6 @@
debugImplementation(project(":compose:ui:ui-tooling"))
}
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
-}
-
android {
compileSdk 35
namespace "androidx.compose.animation.demos"
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/CraneDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/CraneDemo.kt
index 375751a..803c694 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/CraneDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/CraneDemo.kt
@@ -70,7 +70,10 @@
val avatar = remember {
movableContentWithReceiverOf<SceneScope> {
Box(
- Modifier.sharedElementBasedOnProgress(progressProvider)
+ Modifier.sharedElementBasedOnProgress(
+ this@movableContentWithReceiverOf,
+ progressProvider
+ )
.background(Color(0xffff6f69), RoundedCornerShape(20))
.fillMaxSize()
)
@@ -81,7 +84,10 @@
movableContentWithReceiverOf<SceneScope, @Composable () -> Unit> { child ->
Surface(
modifier =
- Modifier.sharedElementBasedOnProgress(progressProvider)
+ Modifier.sharedElementBasedOnProgress(
+ this@movableContentWithReceiverOf,
+ progressProvider
+ )
.background(Color(0xfffdedac)),
color = Color(0xfffdedac),
shape = RoundedCornerShape(10.dp)
@@ -166,44 +172,53 @@
val progress: Float
}
-context(LookaheadScope)
@SuppressLint("PrimitiveInCollection")
-fun <T> Modifier.sharedElementBasedOnProgress(provider: ProgressProvider<T>) = composed {
- val sizeMap = remember { mutableMapOf<T, IntSize>() }
- val offsetMap = remember { mutableMapOf<T, Offset>() }
- val calculateSize: (IntSize) -> IntSize = {
- sizeMap[provider.targetState] = it
- val (width, height) =
- lerp(
- sizeMap[provider.initialState]!!.toSize(),
- sizeMap[provider.targetState]!!.toSize(),
- provider.progress
- )
- IntSize(width.roundToInt(), height.roundToInt())
- }
-
- val calculateOffset: Placeable.PlacementScope.(ApproachMeasureScope) -> IntOffset = {
- with(it) {
- coordinates?.let {
- offsetMap[provider.targetState] =
- lookaheadScopeCoordinates.localLookaheadPositionOf(it)
- val lerpedOffset =
+fun <T> Modifier.sharedElementBasedOnProgress(
+ lookaheadScope: LookaheadScope,
+ provider: ProgressProvider<T>
+) =
+ with(lookaheadScope) {
+ composed {
+ val sizeMap = remember { mutableMapOf<T, IntSize>() }
+ val offsetMap = remember { mutableMapOf<T, Offset>() }
+ val calculateSize: (IntSize) -> IntSize = {
+ sizeMap[provider.targetState] = it
+ val (width, height) =
lerp(
- offsetMap[provider.initialState]!!,
- offsetMap[provider.targetState]!!,
+ sizeMap[provider.initialState]!!.toSize(),
+ sizeMap[provider.targetState]!!.toSize(),
provider.progress
)
- val currentOffset = lookaheadScopeCoordinates.localPositionOf(it, Offset.Zero)
- (lerpedOffset - currentOffset).round()
- } ?: IntOffset(0, 0)
+ IntSize(width.roundToInt(), height.roundToInt())
+ }
+
+ val calculateOffset: Placeable.PlacementScope.(ApproachMeasureScope) -> IntOffset = {
+ with(it) {
+ coordinates?.let {
+ offsetMap[provider.targetState] =
+ lookaheadScopeCoordinates.localLookaheadPositionOf(it)
+ val lerpedOffset =
+ lerp(
+ offsetMap[provider.initialState]!!,
+ offsetMap[provider.targetState]!!,
+ provider.progress
+ )
+ val currentOffset =
+ lookaheadScopeCoordinates.localPositionOf(
+ it,
+ androidx.compose.ui.geometry.Offset.Zero
+ )
+ (lerpedOffset - currentOffset).round()
+ } ?: IntOffset(0, 0)
+ }
+ }
+ this.approachLayout({ provider.progress != 1f }) { measurable, _ ->
+ val (width, height) = calculateSize(lookaheadSize)
+ val animatedConstraints = androidx.compose.ui.unit.Constraints.fixed(width, height)
+ val placeable = measurable.measure(animatedConstraints)
+ layout(placeable.width, placeable.height) {
+ placeable.place(calculateOffset(this@approachLayout))
+ }
+ }
}
}
- this.approachLayout({ provider.progress != 1f }) { measurable, _ ->
- val (width, height) = calculateSize(lookaheadSize)
- val animatedConstraints = Constraints.fixed(width, height)
- val placeable = measurable.measure(animatedConstraints)
- layout(placeable.width, placeable.height) {
- placeable.place(calculateOffset(this@approachLayout))
- }
- }
-}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithDisappearingMoveableContentDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithDisappearingMoveableContentDemo.kt
index 5562c9c..0879163 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithDisappearingMoveableContentDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithDisappearingMoveableContentDemo.kt
@@ -71,7 +71,9 @@
Box(Modifier.padding(start = 50.dp, top = 200.dp, bottom = 100.dp)) {
val icon = remember { movableContentOf<Boolean> { MyIcon(it) } }
val title = remember {
- movableContentOf<Boolean> { Title(visible = it, Modifier.animatePosition()) }
+ movableContentOf<Boolean> {
+ Title(visible = it, Modifier.animatePosition(this@LookaheadScope))
+ }
}
val details = remember { movableContentOf<Boolean> { Details(visible = it) } }
@@ -129,39 +131,41 @@
}
}
-context(LookaheadScope)
@OptIn(ExperimentalAnimatableApi::class)
@SuppressLint("UnnecessaryComposedModifier")
-fun Modifier.animatePosition(): Modifier = composed {
- val offsetAnimation = remember { DeferredTargetAnimation(IntOffset.VectorConverter) }
- val coroutineScope = rememberCoroutineScope()
- this.approachLayout(
- isMeasurementApproachInProgress = { false },
- isPlacementApproachInProgress = {
- offsetAnimation.updateTarget(
- lookaheadScopeCoordinates.localLookaheadPositionOf(it).round(),
- coroutineScope,
- spring(stiffness = Spring.StiffnessMediumLow)
- )
- !offsetAnimation.isIdle
- }
- ) { measurable, constraints ->
- measurable.measure(constraints).run {
- layout(width, height) {
- val (x, y) =
- coordinates?.let { coordinates ->
- val origin = this.lookaheadScopeCoordinates
- val animOffset =
- offsetAnimation.updateTarget(
- origin.localLookaheadPositionOf(coordinates).round(),
- coroutineScope,
- spring(stiffness = Spring.StiffnessMediumLow),
- )
- val currentOffset = origin.localPositionOf(coordinates, Offset.Zero)
- animOffset - currentOffset.round()
- } ?: IntOffset.Zero
- place(x, y)
+fun Modifier.animatePosition(lookaheadScope: LookaheadScope): Modifier =
+ with(lookaheadScope) {
+ composed {
+ val offsetAnimation = remember { DeferredTargetAnimation(IntOffset.VectorConverter) }
+ val coroutineScope = rememberCoroutineScope()
+ this.approachLayout(
+ isMeasurementApproachInProgress = { false },
+ isPlacementApproachInProgress = {
+ offsetAnimation.updateTarget(
+ lookaheadScopeCoordinates.localLookaheadPositionOf(it).round(),
+ coroutineScope,
+ spring(stiffness = Spring.StiffnessMediumLow)
+ )
+ !offsetAnimation.isIdle
+ }
+ ) { measurable, constraints ->
+ measurable.measure(constraints).run {
+ layout(width, height) {
+ val (x, y) =
+ coordinates?.let { coordinates ->
+ val origin = this.lookaheadScopeCoordinates
+ val animOffset =
+ offsetAnimation.updateTarget(
+ origin.localLookaheadPositionOf(coordinates).round(),
+ coroutineScope,
+ spring(stiffness = Spring.StiffnessMediumLow),
+ )
+ val currentOffset = origin.localPositionOf(coordinates, Offset.Zero)
+ animOffset - currentOffset.round()
+ } ?: IntOffset.Zero
+ place(x, y)
+ }
+ }
}
}
}
-}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithMovableContentDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithMovableContentDemo.kt
index a02fc9a..d294736 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithMovableContentDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithMovableContentDemo.kt
@@ -18,10 +18,6 @@
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.animateBounds
-import androidx.compose.animation.core.DeferredTargetAnimation
-import androidx.compose.animation.core.ExperimentalAnimatableApi
-import androidx.compose.animation.core.VectorConverter
-import androidx.compose.animation.core.spring
import androidx.compose.animation.demos.fancy.AnimatedDotsDemo
import androidx.compose.animation.demos.statetransition.InfiniteProgress
import androidx.compose.animation.demos.statetransition.InfinitePulsingHeart
@@ -43,22 +39,14 @@
import androidx.compose.runtime.movableContentWithReceiverOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.LookaheadScope
-import androidx.compose.ui.layout.approachLayout
import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.round
@OptIn(ExperimentalSharedTransitionApi::class)
@Preview
@@ -157,37 +145,5 @@
}
}
-context(LookaheadScope)
-@OptIn(ExperimentalAnimatableApi::class)
-fun Modifier.animateBoundsInScope(): Modifier = composed {
- val sizeAnim = remember { DeferredTargetAnimation(IntSize.VectorConverter) }
- val offsetAnim = remember { DeferredTargetAnimation(IntOffset.VectorConverter) }
- val scope = rememberCoroutineScope()
- this.approachLayout(
- isMeasurementApproachInProgress = {
- sizeAnim.updateTarget(it, scope)
- !sizeAnim.isIdle
- },
- isPlacementApproachInProgress = {
- val target = lookaheadScopeCoordinates.localLookaheadPositionOf(it)
- offsetAnim.updateTarget(target.round(), scope, spring())
- !offsetAnim.isIdle
- }
- ) { measurable, _ ->
- val (animWidth, animHeight) = sizeAnim.updateTarget(lookaheadSize, scope, spring())
- measurable.measure(Constraints.fixed(animWidth, animHeight)).run {
- layout(width, height) {
- coordinates?.let {
- val target = lookaheadScopeCoordinates.localLookaheadPositionOf(it).round()
- val animOffset = offsetAnim.updateTarget(target, scope, spring())
- val current = lookaheadScopeCoordinates.localPositionOf(it, Offset.Zero).round()
- val (x, y) = animOffset - current
- place(x, y)
- } ?: place(0, 0)
- }
- }
- }
-}
-
private val colors =
listOf(Color(0xffff6f69), Color(0xffffcc5c), Color(0xff264653), Color(0xff2a9d84))
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithSubcompose.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithSubcompose.kt
index 2ea3bd2..ed920ea 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithSubcompose.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithSubcompose.kt
@@ -55,13 +55,15 @@
Text(if (shouldAnimate) "Stop animating bounds" else "Animate bounds")
}
SubcomposeLayout(
- Modifier.background(colors[3]).conditionallyAnimateBounds(shouldAnimate)
+ Modifier.background(colors[3])
+ .conditionallyAnimateBounds(this@LookaheadScope, shouldAnimate)
) {
val constraints = it.copy(minWidth = 0)
val placeable =
subcompose(0) {
Box(
Modifier.conditionallyAnimateBounds(
+ this@LookaheadScope,
shouldAnimate,
Modifier.width(if (isWide) 150.dp else 70.dp)
.requiredHeight(400.dp)
@@ -75,6 +77,7 @@
subcompose(1) {
Box(
Modifier.conditionallyAnimateBounds(
+ this@LookaheadScope,
shouldAnimate,
Modifier.width(if (isWide) 150.dp else 70.dp)
.requiredHeight(400.dp)
@@ -91,6 +94,7 @@
Box(
Modifier.width(totalWidth.toDp())
.conditionallyAnimateBounds(
+ this@LookaheadScope,
shouldAnimate,
Modifier.height(if (isWide) 150.dp else 70.dp)
)
@@ -108,12 +112,12 @@
}
}
-context(LookaheadScope)
@OptIn(ExperimentalSharedTransitionApi::class)
private fun Modifier.conditionallyAnimateBounds(
+ lookaheadScope: LookaheadScope,
shouldAnimate: Boolean,
modifier: Modifier = Modifier
-) = if (shouldAnimate) this.animateBounds(this@LookaheadScope, modifier) else this.then(modifier)
+) = if (shouldAnimate) this.animateBounds(lookaheadScope, modifier) else this.then(modifier)
private val colors =
listOf(Color(0xffff6f69), Color(0xffffcc5c), Color(0xff2a9d84), Color(0xff264653))
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/SceneHostExperiment.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/SceneHostExperiment.kt
index 1b94fc1..ecd48cd 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/SceneHostExperiment.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/SceneHostExperiment.kt
@@ -106,6 +106,9 @@
layout(placeable.width, placeable.height) {
val (x, y) =
offsetAnimation.updateTargetBasedOnCoordinates(
+ this@SceneScope,
+ this@layout,
+ this@with,
spring(stiffness = Spring.StiffnessMediumLow),
)
coordinates?.let {
@@ -153,25 +156,32 @@
}
}
-context(LookaheadScope, Placeable.PlacementScope, CoroutineScope)
@OptIn(ExperimentalAnimatableApi::class)
internal fun DeferredTargetAnimation<IntOffset, AnimationVector2D>.updateTargetBasedOnCoordinates(
+ lookaheadScope: LookaheadScope,
+ placementScope: Placeable.PlacementScope,
+ coroutineScope: CoroutineScope,
animationSpec: FiniteAnimationSpec<IntOffset>,
): IntOffset {
- coordinates?.let { coordinates ->
- with(this@PlacementScope) {
- val targetOffset = lookaheadScopeCoordinates.localLookaheadPositionOf(coordinates)
- val animOffset =
- updateTarget(
- targetOffset.round(),
- this@CoroutineScope,
- animationSpec,
- )
- val current =
- lookaheadScopeCoordinates.localPositionOf(coordinates, Offset.Zero).round()
- return (animOffset - current)
+ with(lookaheadScope) {
+ with(placementScope) {
+ coordinates?.let { coordinates ->
+ with(placementScope) {
+ val targetOffset =
+ lookaheadScopeCoordinates.localLookaheadPositionOf(coordinates)
+ val animOffset =
+ updateTarget(
+ targetOffset.round(),
+ coroutineScope,
+ animationSpec,
+ )
+ val current =
+ lookaheadScopeCoordinates.localPositionOf(coordinates, Offset.Zero).round()
+ return (animOffset - current)
+ }
+ }
+
+ return IntOffset.Zero
}
}
-
- return IntOffset.Zero
}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/ContainerTransformDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/ContainerTransformDemo.kt
index 25b32da..78916a7 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/ContainerTransformDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/ContainerTransformDemo.kt
@@ -90,180 +90,208 @@
) {
// TODO: Double check on container transform scrolling
if (it != null) {
- DetailView(model = model, selected = it, model.items[6])
+ DetailView(
+ this@AnimatedContent,
+ this@SharedTransitionLayout,
+ model = model,
+ selected = it,
+ model.items[6]
+ )
} else {
- GridView(model = model)
+ GridView(this@AnimatedContent, this@SharedTransitionLayout, model = model)
}
}
}
}
-context(SharedTransitionScope, AnimatedVisibilityScope)
@Composable
-fun Details(kitty: Kitty) {
- Column(
- Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp)
- .fillMaxHeight()
- .wrapContentHeight(Alignment.Top)
- .fillMaxWidth()
- .background(Color.White)
- .padding(start = 10.dp, end = 10.dp)
- ) {
- Row(verticalAlignment = Alignment.CenterVertically) {
- Column {
- Spacer(Modifier.size(20.dp))
- Text(
- kitty.name,
- fontSize = 25.sp,
- modifier =
- Modifier.padding(start = 10.dp)
- .sharedBounds(
- rememberSharedContentState(key = kitty.name + kitty.id),
- this@AnimatedVisibilityScope
- )
- )
- Text(
- kitty.breed,
- fontSize = 22.sp,
- color = Color.Gray,
- modifier =
- Modifier.padding(start = 10.dp)
- .sharedBounds(
- rememberSharedContentState(key = kitty.breed + kitty.id),
- this@AnimatedVisibilityScope
- )
+fun Details(
+ sharedTransitionScope: SharedTransitionScope,
+ animatedVisibilityScope: AnimatedVisibilityScope,
+ kitty: Kitty
+) {
+ with(sharedTransitionScope) {
+ Column(
+ Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp)
+ .fillMaxHeight()
+ .wrapContentHeight(Alignment.Top)
+ .fillMaxWidth()
+ .background(Color.White)
+ .padding(start = 10.dp, end = 10.dp)
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Column {
+ Spacer(Modifier.size(20.dp))
+ Text(
+ kitty.name,
+ fontSize = 25.sp,
+ modifier =
+ Modifier.padding(start = 10.dp)
+ .sharedBounds(
+ rememberSharedContentState(key = kitty.name + kitty.id),
+ animatedVisibilityScope
+ )
+ )
+ Text(
+ kitty.breed,
+ fontSize = 22.sp,
+ color = Color.Gray,
+ modifier =
+ Modifier.padding(start = 10.dp)
+ .sharedBounds(
+ rememberSharedContentState(key = kitty.breed + kitty.id),
+ animatedVisibilityScope
+ )
+ )
+ Spacer(Modifier.size(10.dp))
+ }
+ Spacer(Modifier.weight(1f))
+ Icon(
+ Icons.Outlined.Favorite,
+ contentDescription = null,
+ Modifier.background(Color(0xffffddee), CircleShape).padding(10.dp)
)
Spacer(Modifier.size(10.dp))
}
- Spacer(Modifier.weight(1f))
- Icon(
- Icons.Outlined.Favorite,
- contentDescription = null,
- Modifier.background(Color(0xffffddee), CircleShape).padding(10.dp)
+ Box(
+ modifier =
+ Modifier.padding(bottom = 10.dp)
+ .height(2.dp)
+ .fillMaxWidth()
+ .background(Color(0xffeeeeee))
)
- Spacer(Modifier.size(10.dp))
+ Text(
+ text =
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent fringilla" +
+ " mollis efficitur. Maecenas sit amet urna eu urna blandit suscipit efficitur" +
+ " eget mauris. Nullam eget aliquet ligula. Nunc id euismod elit. Morbi aliquam" +
+ " enim eros, eget consequat dolor consequat id. Quisque elementum faucibus" +
+ " congue. Curabitur mollis aliquet turpis, ut pellentesque justo eleifend nec.\n" +
+ "\n" +
+ "Suspendisse ac consequat turpis, euismod lacinia quam. Nulla lacinia tellus" +
+ " eu felis tristique ultricies. Vivamus et ultricies dolor. Orci varius" +
+ " natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus." +
+ " Ut gravida porttitor arcu elementum elementum. Phasellus ultrices vel turpis" +
+ " volutpat mollis. Vivamus leo diam, placerat quis leo efficitur, ultrices" +
+ " placerat ex. Nullam mollis et metus ac ultricies. Ut ligula metus, congue" +
+ " gravida metus in, vestibulum posuere velit. Sed et ex nisl. Fusce tempor" +
+ " odio eget sapien pellentesque, sed cursus velit fringilla. Nullam odio" +
+ " ipsum, eleifend non consectetur vitae, congue id libero. Etiam tincidunt" +
+ " mauris at urna dictum ornare.\n" +
+ "\n" +
+ "Etiam at facilisis ex. Sed quis arcu diam. Quisque semper pharetra leo eget" +
+ " fermentum. Nulla dapibus eget mi id porta. Nunc quis sodales nulla, eget" +
+ " commodo sem. Donec lacus enim, pharetra non risus nec, eleifend ultrices" +
+ " augue. Donec sit amet orci porttitor, auctor mauris et, facilisis dolor." +
+ " Nullam mattis luctus orci at pulvinar.\n" +
+ "\n" +
+ "Sed accumsan est massa, ut aliquam nulla dignissim id. Suspendisse in urna" +
+ " condimentum, convallis purus at, molestie nisi. In hac habitasse platea" +
+ " dictumst. Pellentesque id justo quam. Cras iaculis tellus libero, eu" +
+ " feugiat ex pharetra eget. Nunc ultrices, magna ut gravida egestas, mauris" +
+ " justo blandit sapien, eget congue nisi felis congue diam. Mauris at felis" +
+ " vitae erat porta auctor. Pellentesque iaculis sem metus. Phasellus quam" +
+ " neque, congue at est eget, sodales interdum justo. Aenean a pharetra dui." +
+ " Morbi odio nibh, hendrerit vulputate odio eget, sollicitudin egestas ex." +
+ " Fusce nisl ex, fermentum a ultrices id, rhoncus vitae urna. Aliquam quis" +
+ " lobortis turpis.\n" +
+ "\n",
+ color = Color.Gray,
+ fontSize = 15.sp,
+ )
}
- Box(
- modifier =
- Modifier.padding(bottom = 10.dp)
- .height(2.dp)
- .fillMaxWidth()
- .background(Color(0xffeeeeee))
- )
- Text(
- text =
- "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent fringilla" +
- " mollis efficitur. Maecenas sit amet urna eu urna blandit suscipit efficitur" +
- " eget mauris. Nullam eget aliquet ligula. Nunc id euismod elit. Morbi aliquam" +
- " enim eros, eget consequat dolor consequat id. Quisque elementum faucibus" +
- " congue. Curabitur mollis aliquet turpis, ut pellentesque justo eleifend nec.\n" +
- "\n" +
- "Suspendisse ac consequat turpis, euismod lacinia quam. Nulla lacinia tellus" +
- " eu felis tristique ultricies. Vivamus et ultricies dolor. Orci varius" +
- " natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus." +
- " Ut gravida porttitor arcu elementum elementum. Phasellus ultrices vel turpis" +
- " volutpat mollis. Vivamus leo diam, placerat quis leo efficitur, ultrices" +
- " placerat ex. Nullam mollis et metus ac ultricies. Ut ligula metus, congue" +
- " gravida metus in, vestibulum posuere velit. Sed et ex nisl. Fusce tempor" +
- " odio eget sapien pellentesque, sed cursus velit fringilla. Nullam odio" +
- " ipsum, eleifend non consectetur vitae, congue id libero. Etiam tincidunt" +
- " mauris at urna dictum ornare.\n" +
- "\n" +
- "Etiam at facilisis ex. Sed quis arcu diam. Quisque semper pharetra leo eget" +
- " fermentum. Nulla dapibus eget mi id porta. Nunc quis sodales nulla, eget" +
- " commodo sem. Donec lacus enim, pharetra non risus nec, eleifend ultrices" +
- " augue. Donec sit amet orci porttitor, auctor mauris et, facilisis dolor." +
- " Nullam mattis luctus orci at pulvinar.\n" +
- "\n" +
- "Sed accumsan est massa, ut aliquam nulla dignissim id. Suspendisse in urna" +
- " condimentum, convallis purus at, molestie nisi. In hac habitasse platea" +
- " dictumst. Pellentesque id justo quam. Cras iaculis tellus libero, eu" +
- " feugiat ex pharetra eget. Nunc ultrices, magna ut gravida egestas, mauris" +
- " justo blandit sapien, eget congue nisi felis congue diam. Mauris at felis" +
- " vitae erat porta auctor. Pellentesque iaculis sem metus. Phasellus quam" +
- " neque, congue at est eget, sodales interdum justo. Aenean a pharetra dui." +
- " Morbi odio nibh, hendrerit vulputate odio eget, sollicitudin egestas ex." +
- " Fusce nisl ex, fermentum a ultrices id, rhoncus vitae urna. Aliquam quis" +
- " lobortis turpis.\n" +
- "\n",
- color = Color.Gray,
- fontSize = 15.sp,
- )
}
}
-context(AnimatedVisibilityScope, SharedTransitionScope)
-@Suppress("UNUSED_PARAMETER")
@Composable
-fun DetailView(model: MyModel, selected: Kitty, next: Kitty?) {
- Column(
- Modifier.clickable(
- interactionSource = remember { MutableInteractionSource() },
- indication = null
- ) {
- model.selected = null
- }
- .sharedBounds(
- rememberSharedContentState(key = "container + ${selected.id}"),
- this@AnimatedVisibilityScope,
- fadeIn(),
- fadeOut(),
- resizeMode = ScaleToBounds(ContentScale.Crop),
- clipInOverlayDuringTransition = OverlayClip(RoundedCornerShape(20.dp)),
- )
- ) {
- Row(Modifier.fillMaxHeight(0.5f)) {
- Image(
- painter = painterResource(selected.photoResId),
- contentDescription = null,
- contentScale = ContentScale.Crop,
- modifier =
- Modifier.padding(10.dp)
- .sharedElement(
- rememberSharedContentState(key = selected.id),
- this@AnimatedVisibilityScope,
- placeHolderSize = animatedSize
- )
- .fillMaxHeight()
- .aspectRatio(1f)
- .clip(RoundedCornerShape(20.dp))
- )
- if (next != null) {
+fun DetailView(
+ animatedVisibilityScope: AnimatedVisibilityScope,
+ sharedTransitionScope: SharedTransitionScope,
+ model: MyModel,
+ selected: Kitty,
+ next: Kitty?
+) {
+ with(sharedTransitionScope) {
+ Column(
+ Modifier.clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = null
+ ) {
+ model.selected = null
+ }
+ .sharedBounds(
+ rememberSharedContentState(key = "container + ${selected.id}"),
+ animatedVisibilityScope,
+ fadeIn(),
+ fadeOut(),
+ resizeMode = ScaleToBounds(ContentScale.Crop),
+ clipInOverlayDuringTransition = OverlayClip(RoundedCornerShape(20.dp)),
+ )
+ ) {
+ Row(Modifier.fillMaxHeight(0.5f)) {
Image(
- painter = painterResource(next.photoResId),
+ painter = painterResource(selected.photoResId),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier =
- Modifier.padding(top = 10.dp, bottom = 10.dp, end = 10.dp)
- .fillMaxWidth()
+ Modifier.padding(10.dp)
+ .sharedElement(
+ rememberSharedContentState(key = selected.id),
+ animatedVisibilityScope,
+ placeHolderSize = animatedSize
+ )
.fillMaxHeight()
+ .aspectRatio(1f)
.clip(RoundedCornerShape(20.dp))
- .blur(10.dp)
)
+ if (next != null) {
+ Image(
+ painter = painterResource(next.photoResId),
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ modifier =
+ Modifier.padding(top = 10.dp, bottom = 10.dp, end = 10.dp)
+ .fillMaxWidth()
+ .fillMaxHeight()
+ .clip(RoundedCornerShape(20.dp))
+ .blur(10.dp)
+ )
+ }
}
+ Details(sharedTransitionScope, animatedVisibilityScope, kitty = selected)
}
- Details(kitty = selected)
}
}
-context(AnimatedVisibilityScope, SharedTransitionScope)
@Composable
-fun GridView(model: MyModel) {
- Box(Modifier.background(lessVibrantPurple)) {
- Box(
- Modifier.padding(20.dp)
- .renderInSharedTransitionScopeOverlay(zIndexInOverlay = 2f)
- .animateEnterExit(fadeIn(), fadeOut())
- ) {
- SearchBar()
- }
- LazyVerticalGrid(
- columns = GridCells.Fixed(2),
- contentPadding = PaddingValues(top = 90.dp)
- ) {
- items(6) {
- Box(modifier = Modifier.clickable { model.selected = model.items[it] }) {
- KittyItem(model.items[it])
+fun GridView(
+ animatedVisibilityScope: AnimatedVisibilityScope,
+ sharedTransitionScope: SharedTransitionScope,
+ model: MyModel
+) {
+ with(animatedVisibilityScope) {
+ with(sharedTransitionScope) {
+ Box(Modifier.background(lessVibrantPurple)) {
+ Box(
+ Modifier.padding(20.dp)
+ .renderInSharedTransitionScopeOverlay(zIndexInOverlay = 2f)
+ .animateEnterExit(fadeIn(), fadeOut())
+ ) {
+ SearchBar()
+ }
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(2),
+ contentPadding = PaddingValues(top = 90.dp)
+ ) {
+ items(6) {
+ Box(modifier = Modifier.clickable { model.selected = model.items[it] }) {
+ KittyItem(
+ animatedVisibilityScope,
+ sharedTransitionScope,
+ model.items[it]
+ )
+ }
+ }
}
}
}
@@ -284,54 +312,59 @@
var selected: Kitty? by mutableStateOf(null)
}
-context(AnimatedVisibilityScope, SharedTransitionScope)
@Composable
-fun KittyItem(kitty: Kitty) {
- Column(
- Modifier.padding(start = 10.dp, end = 10.dp, bottom = 10.dp)
- .sharedBounds(
- rememberSharedContentState(key = "container + ${kitty.id}"),
- this@AnimatedVisibilityScope,
+fun KittyItem(
+ animatedVisibilityScope: AnimatedVisibilityScope,
+ sharedTransitionScope: SharedTransitionScope,
+ kitty: Kitty
+) {
+ with(sharedTransitionScope) {
+ Column(
+ Modifier.padding(start = 10.dp, end = 10.dp, bottom = 10.dp)
+ .sharedBounds(
+ rememberSharedContentState(key = "container + ${kitty.id}"),
+ animatedVisibilityScope,
+ )
+ .background(Color.White, RoundedCornerShape(20.dp))
+ ) {
+ Image(
+ painter = painterResource(kitty.photoResId),
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ modifier =
+ Modifier.sharedElement(
+ rememberSharedContentState(key = kitty.id),
+ animatedVisibilityScope,
+ placeHolderSize = animatedSize
+ )
+ .aspectRatio(1f)
+ .clip(RoundedCornerShape(20.dp))
)
- .background(Color.White, RoundedCornerShape(20.dp))
- ) {
- Image(
- painter = painterResource(kitty.photoResId),
- contentDescription = null,
- contentScale = ContentScale.Crop,
- modifier =
- Modifier.sharedElement(
- rememberSharedContentState(key = kitty.id),
- this@AnimatedVisibilityScope,
- placeHolderSize = animatedSize
- )
- .aspectRatio(1f)
- .clip(RoundedCornerShape(20.dp))
- )
- Spacer(Modifier.size(10.dp))
- Text(
- kitty.name,
- fontSize = 18.sp,
- modifier =
- Modifier.padding(start = 10.dp)
- .sharedBounds(
- rememberSharedContentState(key = kitty.name + kitty.id),
- this@AnimatedVisibilityScope
- )
- )
- Spacer(Modifier.size(5.dp))
- Text(
- kitty.breed,
- fontSize = 15.sp,
- color = Color.Gray,
- modifier =
- Modifier.padding(start = 10.dp)
- .sharedBounds(
- rememberSharedContentState(key = kitty.breed + kitty.id),
- this@AnimatedVisibilityScope
- )
- )
- Spacer(Modifier.size(10.dp))
+ Spacer(Modifier.size(10.dp))
+ Text(
+ kitty.name,
+ fontSize = 18.sp,
+ modifier =
+ Modifier.padding(start = 10.dp)
+ .sharedBounds(
+ rememberSharedContentState(key = kitty.name + kitty.id),
+ animatedVisibilityScope
+ )
+ )
+ Spacer(Modifier.size(5.dp))
+ Text(
+ kitty.breed,
+ fontSize = 15.sp,
+ color = Color.Gray,
+ modifier =
+ Modifier.padding(start = 10.dp)
+ .sharedBounds(
+ rememberSharedContentState(key = kitty.breed + kitty.id),
+ animatedVisibilityScope
+ )
+ )
+ Spacer(Modifier.size(10.dp))
+ }
}
}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/SwitchBetweenCollapsedAndExpanded.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/SwitchBetweenCollapsedAndExpanded.kt
index 9a3c30d..3cd4c47 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/SwitchBetweenCollapsedAndExpanded.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/SwitchBetweenCollapsedAndExpanded.kt
@@ -83,111 +83,114 @@
)
) {
SharedTransitionLayout {
- HomePage(!showExpandedCard)
- ExpandedCard(showExpandedCard)
+ HomePage(this@SharedTransitionLayout, !showExpandedCard)
+ ExpandedCard(this@SharedTransitionLayout, showExpandedCard)
}
}
}
-context(SharedTransitionScope)
@Composable
-fun HomePage(showCard: Boolean) {
- Box(Modifier.fillMaxSize().background(Color.White)) {
- Column {
- SearchBarAndTabs()
- Box(Modifier.fillMaxWidth().aspectRatio(1.1f)) {
- androidx.compose.animation.AnimatedVisibility(visible = showCard) {
- Column(
- Modifier.padding(top = 10.dp, start = 10.dp, end = 10.dp)
- .sharedBounds(
- rememberSharedContentState(key = "container"),
- this@AnimatedVisibility,
- clipInOverlayDuringTransition =
- OverlayClip(RoundedCornerShape(20.dp))
- )
- .clip(shape = RoundedCornerShape(20.dp))
- .background(color = cardBackgroundColor),
- ) {
- Box {
- Column {
- Image(
- painterResource(R.drawable.quiet_night),
- contentDescription = null,
- modifier =
- Modifier.fillMaxWidth()
- .sharedElement(
- rememberSharedContentState(key = "quiet_night"),
- this@AnimatedVisibility,
- zIndexInOverlay = 0.5f,
- ),
- contentScale = ContentScale.FillWidth
+fun HomePage(sharedTransitionScope: SharedTransitionScope, showCard: Boolean) {
+ with(sharedTransitionScope) {
+ Box(Modifier.fillMaxSize().background(Color.White)) {
+ Column {
+ SearchBarAndTabs()
+ Box(Modifier.fillMaxWidth().aspectRatio(1.1f)) {
+ androidx.compose.animation.AnimatedVisibility(visible = showCard) {
+ Column(
+ Modifier.padding(top = 10.dp, start = 10.dp, end = 10.dp)
+ .sharedBounds(
+ rememberSharedContentState(key = "container"),
+ this@AnimatedVisibility,
+ clipInOverlayDuringTransition =
+ OverlayClip(RoundedCornerShape(20.dp))
)
- Text(
- text = longText,
- color = Color.Gray,
- fontSize = 15.sp,
- modifier =
- Modifier.fillMaxWidth()
- .padding(start = 20.dp, end = 20.dp, top = 20.dp)
- .height(14.dp)
- .sharedElement(
- rememberSharedContentState(key = "longText"),
- this@AnimatedVisibility,
- )
- .clipToBounds()
- .wrapContentHeight(
- align = Alignment.Top,
- unbounded = true
- )
- .skipToLookaheadSize(),
- )
- }
+ .clip(shape = RoundedCornerShape(20.dp))
+ .background(color = cardBackgroundColor),
+ ) {
+ Box {
+ Column {
+ Image(
+ painterResource(R.drawable.quiet_night),
+ contentDescription = null,
+ modifier =
+ Modifier.fillMaxWidth()
+ .sharedElement(
+ rememberSharedContentState(key = "quiet_night"),
+ this@AnimatedVisibility,
+ zIndexInOverlay = 0.5f,
+ ),
+ contentScale = ContentScale.FillWidth
+ )
+ Text(
+ text = longText,
+ color = Color.Gray,
+ fontSize = 15.sp,
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(start = 20.dp, end = 20.dp, top = 20.dp)
+ .height(14.dp)
+ .sharedElement(
+ rememberSharedContentState(key = "longText"),
+ this@AnimatedVisibility,
+ )
+ .clipToBounds()
+ .wrapContentHeight(
+ align = Alignment.Top,
+ unbounded = true
+ )
+ .skipToLookaheadSize(),
+ )
+ }
- Text(
- text = title,
- fontFamily = FontFamily.Default,
- color = Color.White,
- fontSize = 20.sp,
- modifier =
- Modifier.fillMaxWidth()
- .align(Alignment.BottomCenter)
- .renderInSharedTransitionScopeOverlay(zIndexInOverlay = 1f)
- .animateEnterExit(
- fadeIn(tween(1000)) + slideInVertically { -it / 3 },
- fadeOut(tween(50)) + slideOutVertically { -it / 3 }
- )
- .skipToLookaheadSize()
- .background(
- Brush.verticalGradient(
- listOf(
- Color.Transparent,
- Color.Black,
- Color.Transparent
+ Text(
+ text = title,
+ fontFamily = FontFamily.Default,
+ color = Color.White,
+ fontSize = 20.sp,
+ modifier =
+ Modifier.fillMaxWidth()
+ .align(Alignment.BottomCenter)
+ .renderInSharedTransitionScopeOverlay(
+ zIndexInOverlay = 1f
+ )
+ .animateEnterExit(
+ fadeIn(tween(1000)) + slideInVertically { -it / 3 },
+ fadeOut(tween(50)) + slideOutVertically { -it / 3 }
+ )
+ .skipToLookaheadSize()
+ .background(
+ Brush.verticalGradient(
+ listOf(
+ Color.Transparent,
+ Color.Black,
+ Color.Transparent
+ )
)
)
- )
- .padding(20.dp),
+ .padding(20.dp),
+ )
+ }
+ InstallBar(
+ Modifier.fillMaxWidth()
+ .zIndex(1f)
+ .sharedElementWithCallerManagedVisibility(
+ rememberSharedContentState(key = "install_bar"),
+ showCard,
+ )
)
}
- InstallBar(
- Modifier.fillMaxWidth()
- .zIndex(1f)
- .sharedElementWithCallerManagedVisibility(
- rememberSharedContentState(key = "install_bar"),
- showCard,
- )
- )
}
}
+ Cluster()
}
- Cluster()
+ Image(
+ painterResource(R.drawable.navigation_bar),
+ contentDescription = null,
+ Modifier.fillMaxWidth().align(Alignment.BottomCenter),
+ contentScale = ContentScale.FillWidth
+ )
}
- Image(
- painterResource(R.drawable.navigation_bar),
- contentDescription = null,
- Modifier.fillMaxWidth().align(Alignment.BottomCenter),
- contentScale = ContentScale.FillWidth
- )
}
}
@@ -221,97 +224,98 @@
}
}
-context(SharedTransitionScope)
@Composable
-fun ExpandedCard(visible: Boolean) {
- AnimatedVisibility(
- visible = visible,
- Modifier.fillMaxSize(),
- enter = fadeIn(),
- exit = fadeOut()
- ) {
- Box(Modifier.fillMaxSize().background(Color(0x55000000))) {
- Column(
- Modifier.align(Alignment.Center)
- .padding(20.dp)
- .sharedBounds(
- rememberSharedContentState(key = "container"),
- this@AnimatedVisibility,
- enter = EnterTransition.None,
- exit = ExitTransition.None,
- clipInOverlayDuringTransition = OverlayClip(RoundedCornerShape(20.dp))
- )
- .clip(shape = RoundedCornerShape(20.dp))
- .background(cardBackgroundColor)
- ) {
+fun ExpandedCard(sharedTransitionScope: SharedTransitionScope, visible: Boolean) {
+ with(sharedTransitionScope) {
+ AnimatedVisibility(
+ visible = visible,
+ Modifier.fillMaxSize(),
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
+ Box(Modifier.fillMaxSize().background(Color(0x55000000))) {
Column(
- Modifier.renderInSharedTransitionScopeOverlay(zIndexInOverlay = 1f)
- .animateEnterExit(
- fadeIn() + slideInVertically { it / 3 },
- fadeOut() + slideOutVertically { it / 3 }
+ Modifier.align(Alignment.Center)
+ .padding(20.dp)
+ .sharedBounds(
+ rememberSharedContentState(key = "container"),
+ this@AnimatedVisibility,
+ enter = EnterTransition.None,
+ exit = ExitTransition.None,
+ clipInOverlayDuringTransition = OverlayClip(RoundedCornerShape(20.dp))
)
- .skipToLookaheadSize()
- .background(
- Brush.verticalGradient(
- listOf(Color.Transparent, Color.Black, Color.Transparent)
- )
- )
- .padding(start = 20.dp, end = 20.dp),
+ .clip(shape = RoundedCornerShape(20.dp))
+ .background(cardBackgroundColor)
) {
- Text(
- text = "Lorem ipsum",
- Modifier.padding(top = 20.dp, bottom = 10.dp)
- .background(Color.LightGray, shape = RoundedCornerShape(15.dp))
- .padding(top = 8.dp, bottom = 8.dp, start = 15.dp, end = 15.dp),
- color = Color.Black,
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Bold,
- fontSize = 15.sp
+ Column(
+ Modifier.renderInSharedTransitionScopeOverlay(zIndexInOverlay = 1f)
+ .animateEnterExit(
+ fadeIn() + slideInVertically { it / 3 },
+ fadeOut() + slideOutVertically { it / 3 }
+ )
+ .skipToLookaheadSize()
+ .background(
+ Brush.verticalGradient(
+ listOf(Color.Transparent, Color.Black, Color.Transparent)
+ )
+ )
+ .padding(start = 20.dp, end = 20.dp),
+ ) {
+ Text(
+ text = "Lorem ipsum",
+ Modifier.padding(top = 20.dp, bottom = 10.dp)
+ .background(Color.LightGray, shape = RoundedCornerShape(15.dp))
+ .padding(top = 8.dp, bottom = 8.dp, start = 15.dp, end = 15.dp),
+ color = Color.Black,
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Bold,
+ fontSize = 15.sp
+ )
+ Text(
+ text = title,
+ color = Color.White,
+ fontSize = 30.sp,
+ modifier = Modifier.fillMaxWidth().padding(bottom = 20.dp)
+ )
+ }
+ Image(
+ painterResource(R.drawable.quiet_night),
+ contentDescription = null,
+ modifier =
+ Modifier.fillMaxWidth()
+ .sharedElement(
+ rememberSharedContentState("quiet_night"),
+ this@AnimatedVisibility,
+ ),
+ contentScale = ContentScale.FillWidth
)
+
Text(
- text = title,
- color = Color.White,
- fontSize = 30.sp,
- modifier = Modifier.fillMaxWidth().padding(bottom = 20.dp)
+ text = longText,
+ color = Color.Gray,
+ fontSize = 15.sp,
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(start = 15.dp, end = 10.dp, top = 10.dp)
+ .height(50.dp)
+ .sharedElement(
+ rememberSharedContentState("longText"),
+ this@AnimatedVisibility,
+ )
+ .clipToBounds()
+ .wrapContentHeight(align = Alignment.Top, unbounded = true)
+ .skipToLookaheadSize(),
+ )
+
+ InstallBar(
+ Modifier.fillMaxWidth()
+ .zIndex(1f)
+ .sharedElement(
+ rememberSharedContentState("install_bar"),
+ this@AnimatedVisibility,
+ )
)
}
- Image(
- painterResource(R.drawable.quiet_night),
- contentDescription = null,
- modifier =
- Modifier.fillMaxWidth()
- .sharedElement(
- rememberSharedContentState("quiet_night"),
- this@AnimatedVisibility,
- ),
- contentScale = ContentScale.FillWidth
- )
-
- Text(
- text = longText,
- color = Color.Gray,
- fontSize = 15.sp,
- modifier =
- Modifier.fillMaxWidth()
- .padding(start = 15.dp, end = 10.dp, top = 10.dp)
- .height(50.dp)
- .sharedElement(
- rememberSharedContentState("longText"),
- this@AnimatedVisibility,
- )
- .clipToBounds()
- .wrapContentHeight(align = Alignment.Top, unbounded = true)
- .skipToLookaheadSize(),
- )
-
- InstallBar(
- Modifier.fillMaxWidth()
- .zIndex(1f)
- .sharedElement(
- rememberSharedContentState("install_bar"),
- this@AnimatedVisibility,
- )
- )
}
}
}
diff --git a/compose/benchmark-utils/build.gradle b/compose/benchmark-utils/build.gradle
index b994260..fd97201 100644
--- a/compose/benchmark-utils/build.gradle
+++ b/compose/benchmark-utils/build.gradle
@@ -33,17 +33,17 @@
dependencies {
api("androidx.activity:activity:1.2.0")
api(project(":compose:test-utils"))
- api(projectOrArtifact(":benchmark:benchmark-junit4"))
+ api(project(":benchmark:benchmark-junit4"))
implementation(libs.kotlinStdlibCommon)
- implementation(projectOrArtifact(":compose:runtime:runtime"))
- implementation(projectOrArtifact(":compose:ui:ui"))
+ implementation(project(":compose:runtime:runtime"))
+ implementation(project(":compose:ui:ui"))
implementation(project(":tracing:tracing-ktx"))
implementation(libs.testRules)
// This has stub APIs for access to legacy Android APIs, so we don't want
// any dependency on this module.
- compileOnly(projectOrArtifact(":compose:ui:ui-android-stubs"))
+ compileOnly(project(":compose:ui:ui-android-stubs"))
}
tasks.withType(KotlinCompile).configureEach {
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeText.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeText.kt
index 6152c8f..4169d00 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeText.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeText.kt
@@ -16,6 +16,7 @@
package androidx.compose.foundation.demos.text
+import android.annotation.SuppressLint
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.infiniteRepeatable
@@ -35,11 +36,15 @@
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup
+import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
@@ -100,7 +105,9 @@
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
@@ -120,8 +127,26 @@
val fontSize8 = 25.sp
val fontSize10 = 30.sp
-private val overflowOptions = listOf(TextOverflow.Visible, TextOverflow.Ellipsis, TextOverflow.Clip)
-private val paragraphOptions = listOf(true, false)
+@SuppressLint("PrimitiveInCollection")
+private val overflowOptions =
+ listOf(
+ TextOverflow.Clip,
+ TextOverflow.Visible,
+ TextOverflow.StartEllipsis,
+ TextOverflow.MiddleEllipsis,
+ TextOverflow.Ellipsis
+ )
+private val boolOptions = listOf(true, false)
+@SuppressLint("PrimitiveInCollection")
+private val textAlignments =
+ listOf(
+ TextAlign.Left,
+ TextAlign.Start,
+ TextAlign.Center,
+ TextAlign.Right,
+ TextAlign.End,
+ TextAlign.Justify
+ )
@Preview
@Composable
@@ -623,9 +648,30 @@
@Composable
fun TextOverflowDemo() {
- Column {
- var singleParagraph by remember { mutableStateOf(paragraphOptions[0]) }
+ Column(
+ verticalArrangement = Arrangement.spacedBy(10.dp),
+ modifier = Modifier.verticalScroll(rememberScrollState())
+ ) {
+ var singleParagraph by remember { mutableStateOf(boolOptions[0]) }
var selectedOverflow by remember { mutableStateOf(overflowOptions[0]) }
+ var singleLinePerPar by remember { mutableStateOf(boolOptions[1]) }
+ var width by remember { mutableFloatStateOf(250f) }
+ var height by remember { mutableFloatStateOf(50f) }
+ var letterSpacing by remember { mutableFloatStateOf(0f) }
+ var textAlign by remember { mutableStateOf(TextAlign.Left) }
+ var softWrap by remember { mutableStateOf(true) }
+
+ TextOverflowDemo(
+ singleParagraph,
+ selectedOverflow,
+ singleLinePerPar,
+ width.dp,
+ height.dp,
+ letterSpacing.sp,
+ textAlign,
+ softWrap
+ )
+
Row(Modifier.fillMaxWidth()) {
Column(Modifier.selectableGroup().weight(1f)) {
Text("TextOverflow", fontWeight = FontWeight.Bold)
@@ -649,7 +695,7 @@
}
Column(Modifier.selectableGroup().weight(1f)) {
Text("Paragraph", fontWeight = FontWeight.Bold)
- paragraphOptions.forEach {
+ boolOptions.forEach {
Row(
Modifier.fillMaxWidth()
.selectable(
@@ -667,14 +713,86 @@
}
}
}
+ Column(Modifier.selectableGroup().weight(1f)) {
+ Text("Single line", fontWeight = FontWeight.Bold)
+ boolOptions.forEach {
+ Row(
+ Modifier.fillMaxWidth()
+ .selectable(
+ selected = (it == singleLinePerPar),
+ onClick = { singleLinePerPar = it },
+ role = Role.RadioButton
+ ),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ RadioButton(selected = (it == singleLinePerPar), onClick = null)
+ Text(text = it.toString())
+ }
+ }
+ }
}
-
- TextOverflowDemo(singleParagraph, selectedOverflow)
+ Column {
+ Text("Width " + "%.1f".format(width) + "dp")
+ Slider(width, { width = it }, valueRange = 30f..300f)
+ }
+ Column {
+ Text("Height " + "%.1f".format(height) + "dp")
+ Slider(height, { height = it }, valueRange = 5f..300f)
+ }
+ Column {
+ Text("Letter spacing " + "%.1f".format(letterSpacing) + "sp")
+ Slider(letterSpacing, { letterSpacing = it }, valueRange = -4f..8f, steps = 11)
+ }
+ Row(Modifier.fillMaxWidth()) {
+ Column(Modifier.weight(1f)) {
+ Text("Text Align", fontWeight = FontWeight.Bold)
+ textAlignments.forEach {
+ Row(
+ Modifier.fillMaxWidth()
+ .selectable(
+ selected = (it == textAlign),
+ onClick = { textAlign = it },
+ role = Role.RadioButton
+ ),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ RadioButton(selected = (it == textAlign), onClick = null)
+ Text(text = it.toString())
+ }
+ }
+ }
+ Column(Modifier.weight(1f)) {
+ Text("Soft wrap", fontWeight = FontWeight.Bold)
+ boolOptions.forEach {
+ Row(
+ Modifier.fillMaxWidth()
+ .selectable(
+ selected = (it == softWrap),
+ onClick = { softWrap = it },
+ role = Role.RadioButton
+ ),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ RadioButton(selected = (it == softWrap), onClick = null)
+ Text(text = it.toString())
+ }
+ }
+ }
+ }
}
}
@Composable
-private fun ColumnScope.TextOverflowDemo(singleParagraph: Boolean, textOverflow: TextOverflow) {
+private fun ColumnScope.TextOverflowDemo(
+ singleParagraph: Boolean,
+ textOverflow: TextOverflow,
+ singeLine: Boolean,
+ width: Dp,
+ height: Dp,
+ letterSpacing: TextUnit,
+ textAlign: TextAlign,
+ softWrap: Boolean
+) {
Box(Modifier.weight(1f).fillMaxWidth()) {
val text =
if (singleParagraph) {
@@ -687,11 +805,19 @@
}
}
}
- Text(
+ val textStyle =
+ TextStyle(fontSize = fontSize6, letterSpacing = letterSpacing, textAlign = textAlign)
+ BasicText(
text = text,
- modifier = Modifier.align(Alignment.Center).background(Color.Magenta).size(100.dp),
- fontSize = fontSize6,
- overflow = textOverflow
+ modifier =
+ Modifier.align(Alignment.Center)
+ .background(Color.Magenta)
+ .widthIn(max = width)
+ .heightIn(max = height),
+ style = textStyle,
+ overflow = textOverflow,
+ maxLines = if (singeLine) 1 else Int.MAX_VALUE,
+ softWrap = softWrap
)
}
}
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListAnimateScrollScope.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListAnimateScrollScope.kt
new file mode 100644
index 0000000..1d465b1
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListAnimateScrollScope.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.foundation.lazy.list
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyLayoutAnimateScrollScope
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+class LazyListAnimateScrollScope(orientation: Orientation) :
+ BaseLazyListTestWithOrientation(orientation) {
+
+ @Test
+ fun animateToItem_stickyHeader_shouldNotConsiderItemFound() {
+ lateinit var state: LazyListState
+ rule.setContent {
+ state = rememberLazyListState(initialFirstVisibleItemIndex = 3)
+ LazyColumnOrRow(Modifier.crossAxisSize(150.dp).mainAxisSize(100.dp), state) {
+ stickyHeader { Box(Modifier.size(150.dp)) }
+ items(20) { Box(Modifier.size(150.dp)) }
+ }
+ }
+
+ val animatedScrollScope = LazyLayoutAnimateScrollScope(state)
+ /**
+ * Sticky item is considered non visible whilst sticking, distance should be best effort,
+ * average size * (target pos - current pos)
+ */
+ assertThat(animatedScrollScope.calculateDistanceTo(0))
+ .isEqualTo(-3 * with(rule.density) { 150.dp.roundToPx() })
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun params() = arrayOf(Orientation.Vertical, Orientation.Horizontal)
+ }
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/Draggable2DTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/Draggable2DTest.kt
index cdd939a..5493358 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/Draggable2DTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/Draggable2DTest.kt
@@ -808,14 +808,12 @@
val moveAngle = Math.atan(moveOffset.x / moveOffset.y.toDouble())
rule.runOnIdle {
- assertEquals(
- downEventPosition.x + touchSlop * Math.cos(moveAngle).toFloat(),
- onDragStartedOffset.x
- )
- assertEquals(
- downEventPosition.y + touchSlop * Math.sin(moveAngle).toFloat(),
- onDragStartedOffset.y
- )
+ assertThat(downEventPosition.x + touchSlop * Math.cos(moveAngle).toFloat())
+ .isWithin(0.5f)
+ .of(onDragStartedOffset.x)
+ assertThat(downEventPosition.y + touchSlop * Math.sin(moveAngle).toFloat())
+ .isWithin(0.5f)
+ .of(onDragStartedOffset.y)
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gestures/AwaitTouchEventTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gestures/AwaitLongPressOrCancellationTest.kt
similarity index 75%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gestures/AwaitTouchEventTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gestures/AwaitLongPressOrCancellationTest.kt
index 3d2c626..498c9df 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gestures/AwaitTouchEventTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gestures/AwaitLongPressOrCancellationTest.kt
@@ -16,6 +16,15 @@
package androidx.compose.foundation.gestures
+import android.os.Build
+import android.view.InputDevice
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_MOVE
+import android.view.MotionEvent.CLASSIFICATION_DEEP_PRESS
+import android.view.MotionEvent.CLASSIFICATION_NONE
+import android.view.MotionEvent.PointerCoords
+import android.view.View
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.text.BasicText
import androidx.compose.ui.Modifier
@@ -23,6 +32,7 @@
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.click
@@ -32,6 +42,7 @@
import androidx.compose.ui.test.performTouchInput
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth
import org.junit.After
@@ -339,4 +350,95 @@
rule.runOnIdle { Truth.assertThat(counter).isEqualTo(0) }
}
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ fun awaitLongPressOrCancellationTest_deepPress_assertTriggers() {
+ var counter = 0
+
+ lateinit var view: View
+ rule.setContent {
+ view = LocalView.current
+ Box {
+ BasicText(
+ "LongPressText",
+ modifier =
+ Modifier.testTag("myLongPress").pointerInput(Unit) {
+ awaitEachGesture {
+ val down = awaitFirstDown(requireUnconsumed = false)
+ awaitLongPressOrCancellation(down.id)?.let { counter++ }
+ }
+ }
+ )
+ }
+ }
+
+ rule.runOnIdle { Truth.assertThat(counter).isEqualTo(0) }
+
+ val pointerProperties =
+ arrayOf(
+ MotionEvent.PointerProperties().also {
+ it.id = 0
+ it.toolType = MotionEvent.TOOL_TYPE_FINGER
+ }
+ )
+
+ val downEvent =
+ MotionEvent.obtain(
+ /* downTime = */ 0,
+ /* eventTime = */ 0,
+ /* action = */ ACTION_DOWN,
+ /* pointerCount = */ 1,
+ /* pointerProperties = */ pointerProperties,
+ /* pointerCoords = */ arrayOf(
+ PointerCoords().apply {
+ x = 5f
+ y = 5f
+ }
+ ),
+ /* metaState = */ 0,
+ /* buttonState = */ 0,
+ /* xPrecision = */ 0f,
+ /* yPrecision = */ 0f,
+ /* deviceId = */ 0,
+ /* edgeFlags = */ 0,
+ /* source = */ InputDevice.SOURCE_TOUCHSCREEN,
+ /* displayId = */ 0,
+ /* flags = */ 0,
+ /* classification = */ CLASSIFICATION_NONE
+ )
+
+ view.dispatchTouchEvent(downEvent)
+ rule.mainClock.advanceTimeBy(50)
+
+ val deepPressMoveEvent =
+ MotionEvent.obtain(
+ /* downTime = */ 0,
+ /* eventTime = */ 50,
+ /* action = */ ACTION_MOVE,
+ /* pointerCount = */ 1,
+ /* pointerProperties = */ pointerProperties,
+ /* pointerCoords = */ arrayOf(
+ PointerCoords().apply {
+ x = 10f
+ y = 10f
+ }
+ ),
+ /* metaState = */ 0,
+ /* buttonState = */ 0,
+ /* xPrecision = */ 0f,
+ /* yPrecision = */ 0f,
+ /* deviceId = */ 0,
+ /* edgeFlags = */ 0,
+ /* source = */ InputDevice.SOURCE_TOUCHSCREEN,
+ /* displayId = */ 0,
+ /* flags = */ 0,
+ /* classification = */ CLASSIFICATION_DEEP_PRESS
+ )
+
+ view.dispatchTouchEvent(deepPressMoveEvent)
+ rule.mainClock.advanceTimeBy(50)
+
+ rule.runOnIdle { Truth.assertThat(counter).isEqualTo(1) }
+ }
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gestures/TapGestureDetectorTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gestures/TapGestureDetectorTest.kt
index cb0ca97..f29a00b 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gestures/TapGestureDetectorTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gestures/TapGestureDetectorTest.kt
@@ -16,6 +16,15 @@
package androidx.compose.foundation.gestures
+import android.os.Build
+import android.view.InputDevice
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_MOVE
+import android.view.MotionEvent.CLASSIFICATION_DEEP_PRESS
+import android.view.MotionEvent.CLASSIFICATION_NONE
+import android.view.MotionEvent.PointerCoords
+import android.view.View
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
@@ -31,6 +40,7 @@
import androidx.compose.ui.input.pointer.PointerInputScope
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.TouchInjectionScope
@@ -39,6 +49,7 @@
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.DpSize
+import androidx.test.filters.SdkSuppress
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
@@ -756,9 +767,203 @@
assertTrue(longPressed)
assertFalse(released)
assertFalse(canceled)
+ assertFalse(doubleTapped)
rule.mainClock.advanceTimeBy(500)
performTouch { up(1) }
assertTrue(released)
}
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ fun longPress_deepPress() {
+ lateinit var view: View
+ rule.setContent {
+ view = LocalView.current
+ allGestures()
+ }
+
+ rule.waitForIdle()
+
+ val pointerProperties =
+ arrayOf(
+ MotionEvent.PointerProperties().also {
+ it.id = 0
+ it.toolType = MotionEvent.TOOL_TYPE_FINGER
+ }
+ )
+
+ val downEvent =
+ MotionEvent.obtain(
+ /* downTime = */ 0,
+ /* eventTime = */ 0,
+ /* action = */ ACTION_DOWN,
+ /* pointerCount = */ 1,
+ /* pointerProperties = */ pointerProperties,
+ /* pointerCoords = */ arrayOf(
+ PointerCoords().apply {
+ x = 5f
+ y = 5f
+ }
+ ),
+ /* metaState = */ 0,
+ /* buttonState = */ 0,
+ /* xPrecision = */ 0f,
+ /* yPrecision = */ 0f,
+ /* deviceId = */ 0,
+ /* edgeFlags = */ 0,
+ /* source = */ InputDevice.SOURCE_TOUCHSCREEN,
+ /* displayId = */ 0,
+ /* flags = */ 0,
+ /* classification = */ CLASSIFICATION_NONE
+ )
+
+ view.dispatchTouchEvent(downEvent)
+ rule.mainClock.advanceTimeBy(50)
+
+ assertTrue(pressed)
+ assertFalse(longPressed)
+
+ val deepPressMoveEvent =
+ MotionEvent.obtain(
+ /* downTime = */ 0,
+ /* eventTime = */ 50,
+ /* action = */ ACTION_MOVE,
+ /* pointerCount = */ 1,
+ /* pointerProperties = */ pointerProperties,
+ /* pointerCoords = */ arrayOf(
+ PointerCoords().apply {
+ x = 10f
+ y = 10f
+ }
+ ),
+ /* metaState = */ 0,
+ /* buttonState = */ 0,
+ /* xPrecision = */ 0f,
+ /* yPrecision = */ 0f,
+ /* deviceId = */ 0,
+ /* edgeFlags = */ 0,
+ /* source = */ InputDevice.SOURCE_TOUCHSCREEN,
+ /* displayId = */ 0,
+ /* flags = */ 0,
+ /* classification = */ CLASSIFICATION_DEEP_PRESS
+ )
+
+ view.dispatchTouchEvent(deepPressMoveEvent)
+ rule.mainClock.advanceTimeBy(50)
+
+ assertTrue(pressed)
+ // Even though the timeout didn't pass, the deep press should immediately trigger the long
+ // press
+ assertTrue(longPressed)
+ assertFalse(tapped)
+ assertFalse(released)
+ assertFalse(canceled)
+ assertFalse(doubleTapped)
+ }
+
+ /** Detect the second deep press as long press. */
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ fun secondTapLongPress_deepPress() {
+ lateinit var view: View
+ rule.setContent {
+ view = LocalView.current
+ allGestures()
+ }
+
+ performTouch {
+ down(0, Offset(5f, 5f))
+ up(0)
+ }
+
+ assertTrue(pressed)
+ assertTrue(released)
+ assertFalse(canceled)
+ assertFalse(tapped)
+ assertFalse(doubleTapped)
+ assertFalse(longPressed)
+
+ pressed = false
+ released = false
+
+ rule.mainClock.advanceTimeBy(50)
+
+ val pointerProperties =
+ arrayOf(
+ MotionEvent.PointerProperties().also {
+ it.id = 0
+ it.toolType = MotionEvent.TOOL_TYPE_FINGER
+ }
+ )
+
+ val downEvent =
+ MotionEvent.obtain(
+ /* downTime = */ 0,
+ /* eventTime = */ 50,
+ /* action = */ ACTION_DOWN,
+ /* pointerCount = */ 1,
+ /* pointerProperties = */ pointerProperties,
+ /* pointerCoords = */ arrayOf(
+ PointerCoords().apply {
+ x = 5f
+ y = 5f
+ }
+ ),
+ /* metaState = */ 0,
+ /* buttonState = */ 0,
+ /* xPrecision = */ 0f,
+ /* yPrecision = */ 0f,
+ /* deviceId = */ 0,
+ /* edgeFlags = */ 0,
+ /* source = */ InputDevice.SOURCE_TOUCHSCREEN,
+ /* displayId = */ 0,
+ /* flags = */ 0,
+ /* classification = */ CLASSIFICATION_NONE
+ )
+
+ view.dispatchTouchEvent(downEvent)
+ rule.mainClock.advanceTimeBy(50)
+
+ assertTrue(pressed)
+ assertFalse(longPressed)
+ assertFalse(tapped)
+
+ val deepPressMoveEvent =
+ MotionEvent.obtain(
+ /* downTime = */ 0,
+ /* eventTime = */ 100,
+ /* action = */ ACTION_MOVE,
+ /* pointerCount = */ 1,
+ /* pointerProperties = */ pointerProperties,
+ /* pointerCoords = */ arrayOf(
+ PointerCoords().apply {
+ x = 10f
+ y = 10f
+ }
+ ),
+ /* metaState = */ 0,
+ /* buttonState = */ 0,
+ /* xPrecision = */ 0f,
+ /* yPrecision = */ 0f,
+ /* deviceId = */ 0,
+ /* edgeFlags = */ 0,
+ /* source = */ InputDevice.SOURCE_TOUCHSCREEN,
+ /* displayId = */ 0,
+ /* flags = */ 0,
+ /* classification = */ CLASSIFICATION_DEEP_PRESS
+ )
+
+ view.dispatchTouchEvent(deepPressMoveEvent)
+ rule.mainClock.advanceTimeBy(50)
+
+ assertTrue(pressed)
+ // Even though the timeout didn't pass, the deep press should immediately trigger the long
+ // press
+ assertTrue(longPressed)
+ assertFalse(tapped)
+ assertFalse(released)
+ assertFalse(canceled)
+ assertFalse(doubleTapped)
+ }
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/CursorAnchorInfoBuilderTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/CursorAnchorInfoBuilderTest.kt
index b79b6e3..a12137b 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/CursorAnchorInfoBuilderTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/CursorAnchorInfoBuilderTest.kt
@@ -690,7 +690,8 @@
style = input.style,
constraints = Constraints(maxWidth = ceil(width).toInt()),
density = input.density,
- fontFamilyResolver = fontFamilyResolver
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = TextOverflow.Clip
)
return TextLayoutResult(input, paragraph, IntSize(intWidth, ceil(paragraph.height).toInt()))
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/LegacyCursorAnchorInfoBuilderTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/LegacyCursorAnchorInfoBuilderTest.kt
index 341e2ec..ebbfe63c 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/LegacyCursorAnchorInfoBuilderTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/LegacyCursorAnchorInfoBuilderTest.kt
@@ -698,7 +698,8 @@
style = input.style,
constraints = Constraints(maxWidth = ceil(width).toInt()),
density = input.density,
- fontFamilyResolver = fontFamilyResolver
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = TextOverflow.Clip
)
return TextLayoutResult(input, paragraph, IntSize(intWidth, ceil(paragraph.height).toInt()))
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/LegacyTextInputMethodRequestCursorAnchorInfoTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/LegacyTextInputMethodRequestCursorAnchorInfoTest.kt
index 694f85a..f7c6b06 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/LegacyTextInputMethodRequestCursorAnchorInfoTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/LegacyTextInputMethodRequestCursorAnchorInfoTest.kt
@@ -412,7 +412,8 @@
style = input.style,
constraints = Constraints(maxWidth = width),
density = input.density,
- fontFamilyResolver = fontFamilyResolver
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = TextOverflow.Clip
)
return TextLayoutResult(input, paragraph, IntSize(width, ceil(paragraph.height).toInt()))
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCacheTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCacheTest.kt
index d18722d..abeabd3 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCacheTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCacheTest.kt
@@ -337,7 +337,7 @@
fontFamilyResolver,
text.spanStyles,
maxLines = 5,
- ellipsis = true
+ overflow = TextOverflow.Ellipsis
)
assertThat(actual.height).isEqualTo(expected.height)
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCacheTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCacheTest.kt
index 6f75020..39a5971 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCacheTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCacheTest.kt
@@ -222,6 +222,54 @@
}
@Test
+ fun TextLayoutResult_layout_withStartEllipsis_withoutSoftWrap() {
+ val fontSize = 20f
+ val textDelegate =
+ ParagraphLayoutCache(
+ text = "Hello World! Hello World! Hello World! Hello World!",
+ style = createTextStyle(fontSize = fontSize.sp),
+ fontFamilyResolver = fontFamilyResolver,
+ softWrap = false,
+ overflow = TextOverflow.StartEllipsis,
+ )
+ .also { it.density = density }
+
+ textDelegate.layoutWithConstraints(Constraints.fixed(0, 0), LayoutDirection.Ltr)
+ // Makes width smaller than needed.
+ val width = textDelegate.maxIntrinsicWidth(LayoutDirection.Ltr) / 2
+ val constraints = Constraints(maxWidth = width)
+ textDelegate.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ val layoutResult = textDelegate.paragraph!!
+
+ assertThat(layoutResult.lineCount).isEqualTo(1)
+ assertThat(layoutResult.isLineEllipsized(0)).isTrue()
+ }
+
+ @Test
+ fun TextLayoutResult_layout_withMiddleEllipsis_withoutSoftWrap() {
+ val fontSize = 20f
+ val textDelegate =
+ ParagraphLayoutCache(
+ text = "Hello World! Hello World! Hello World! Hello World!",
+ style = createTextStyle(fontSize = fontSize.sp),
+ fontFamilyResolver = fontFamilyResolver,
+ softWrap = false,
+ overflow = TextOverflow.MiddleEllipsis,
+ )
+ .also { it.density = density }
+
+ textDelegate.layoutWithConstraints(Constraints.fixed(0, 0), LayoutDirection.Ltr)
+ // Makes width smaller than needed.
+ val width = textDelegate.maxIntrinsicWidth(LayoutDirection.Ltr) / 2
+ val constraints = Constraints(maxWidth = width)
+ textDelegate.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ val layoutResult = textDelegate.paragraph!!
+
+ assertThat(layoutResult.lineCount).isEqualTo(1)
+ assertThat(layoutResult.isLineEllipsized(0)).isTrue()
+ }
+
+ @Test
fun TextLayoutResult_layoutWithLimitedHeight_withEllipsis() {
val fontSize = 20f
@@ -291,7 +339,7 @@
fontFamilyResolver,
emptyList(),
maxLines = 5,
- ellipsis = true
+ overflow = TextOverflow.Ellipsis
)
assertThat(actual.height).isEqualTo(expected.height)
}
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.android.kt
new file mode 100644
index 0000000..7da1ea3
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.android.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.foundation.gestures
+
+import android.view.MotionEvent
+import androidx.compose.ui.input.pointer.PointerEvent
+
+internal actual val PointerEvent.isDeepPress: Boolean
+ get() = classification == MotionEvent.CLASSIFICATION_DEEP_PRESS
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/AutoSizeTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/AutoSizeTest.kt
new file mode 100644
index 0000000..ba7fb8d
--- /dev/null
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/AutoSizeTest.kt
@@ -0,0 +1,204 @@
+/*
+ * 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.foundation.text
+
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.em
+import androidx.compose.ui.unit.sp
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+@SmallTest
+class AutoSizeTest {
+ @Test
+ fun stepBased_valid_args() {
+ // we shouldn't throw here
+ AutoSize.StepBased(1.sp, 2.sp, 3.sp)
+
+ AutoSize.StepBased(0.sp, 0.1.sp, 0.0001.sp)
+
+ AutoSize.StepBased(1.em, 2.em, 0.1.em)
+
+ AutoSize.StepBased(2.sp, 1.em, 0.1.sp)
+ }
+
+ @Test
+ fun stepBased_minFontSize_greaterThan_maxFontSize_coercesTo_maxFontSize() {
+ var autoSize1 = AutoSize.StepBased(2.sp, 1.sp)
+ var autoSize2 = AutoSize.StepBased(1.sp, 1.sp)
+ assertThat(autoSize1).isEqualTo(autoSize2)
+ assertThat(autoSize2).isEqualTo(autoSize1)
+
+ autoSize1 = AutoSize.StepBased(3.6.em, 2.em)
+ autoSize2 = AutoSize.StepBased(2.em, 2.em)
+ assertThat(autoSize1).isEqualTo(autoSize2)
+ assertThat(autoSize2).isEqualTo(autoSize1)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun stepBased_stepSize_tooSmall() {
+ AutoSize.StepBased(0.00000134.sp)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun stepBased_minFontSize_unspecified() {
+ AutoSize.StepBased(TextUnit.Unspecified, 1.sp)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun stepBased_maxFontSize_unspecified() {
+ AutoSize.StepBased(2.sp, TextUnit.Unspecified)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun stepBased_stepSize_unspecified() {
+ AutoSize.StepBased(TextUnit.Unspecified)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun stepBased_minFontSize_negative() {
+ AutoSize.StepBased((-1).sp, 0.sp)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun stepBased_maxFontSize_negative() {
+ AutoSize.StepBased(0.sp, (-1).sp)
+ }
+
+ @Test
+ fun stepBased_equals() {
+ var autoSize1 = AutoSize.StepBased(1.sp, 10.sp, 2.sp)
+ var autoSize2 = AutoSize.StepBased(1.0.sp, 10.0.sp, 2.0.sp)
+ assertThat(autoSize1).isEqualTo(autoSize2)
+ assertThat(autoSize2).isEqualTo(autoSize1)
+
+ autoSize2 = AutoSize.StepBased(1.1.sp, 10.sp, 2.sp)
+ assertThat(autoSize1).isNotEqualTo(autoSize2)
+ assertThat(autoSize2).isNotEqualTo(autoSize1)
+
+ autoSize2 = AutoSize.StepBased(1.sp, 11.1.sp, 2.sp)
+ assertThat(autoSize1).isNotEqualTo(autoSize2)
+ assertThat(autoSize2).isNotEqualTo(autoSize1)
+
+ autoSize2 = AutoSize.StepBased(1.sp, 10.sp, 2.5.sp)
+ assertThat(autoSize1).isNotEqualTo(autoSize2)
+ assertThat(autoSize2).isNotEqualTo(autoSize1)
+
+ autoSize2 = TestAutoSize(7)
+ assertThat(autoSize1).isNotEqualTo(autoSize2)
+
+ autoSize1 = AutoSize.StepBased(1.em, 2.em, 0.1.em)
+ autoSize2 = AutoSize.StepBased(1.0.em, 2.0.em, 0.1.em)
+ assertThat(autoSize1).isEqualTo(autoSize2)
+ assertThat(autoSize2).isEqualTo(autoSize1)
+ }
+
+ @Test
+ fun stepBased_getFontSize_alwaysOverflows() {
+ val autoSize = AutoSize.StepBased(12.sp, 112.sp, 0.25.sp)
+ val searchScope: FontSizeSearchScope = AlwaysOverflows()
+ with(autoSize) { assertThat(searchScope.getFontSize().value).isEqualTo(12) }
+ }
+
+ @Test
+ fun stepBased_getFontSize_neverOverflows() {
+ val autoSize = AutoSize.StepBased(12.sp, 112.sp, 0.25.sp)
+ val searchScope: FontSizeSearchScope = NeverOverflows()
+ with(autoSize) { assertThat(searchScope.getFontSize().value).isEqualTo(112) }
+ }
+
+ @Test
+ fun stepBased_getFontSize_overflowsWhenFontSizeIsGreaterThan60Px() {
+ val autoSize = AutoSize.StepBased(12.sp, 112.sp, 0.25.sp)
+ val searchScope: FontSizeSearchScope = OverflowsWhenFontSizeIsGreaterThan60px()
+ with(autoSize) { assertThat(searchScope.getFontSize().value).isEqualTo(60) }
+ }
+
+ @Test
+ fun stepBased_getFontSize_differentStepSizes() {
+ val autoSize1 = AutoSize.StepBased(10.sp, 100.sp, 10.sp)
+ val autoSize2 = AutoSize.StepBased(10.sp, 100.sp, 20.sp)
+ val searchScope: FontSizeSearchScope = OverflowsWhenFontSizeIsGreaterThan60px()
+
+ with(autoSize1) { assertThat(searchScope.getFontSize().value).isEqualTo(60) }
+ with(autoSize2) { assertThat(searchScope.getFontSize().value).isEqualTo(50) }
+ }
+
+ @Test
+ fun stepBased_getFontSize_stepSize_greaterThan_maxFontSize_minus_minFontSize() {
+ // regardless of the bounds of the container, the only potential font size is minFontSize
+ val autoSize = AutoSize.StepBased(45.sp, 55.sp, 15.sp)
+ with(autoSize) {
+ var searchScope: FontSizeSearchScope = AlwaysOverflows()
+ assertThat(searchScope.getFontSize().value).isEqualTo(45)
+
+ searchScope = NeverOverflows()
+ assertThat(searchScope.getFontSize().value).isEqualTo(45)
+
+ searchScope = OverflowsWhenFontSizeIsGreaterThan60px()
+ assertThat(searchScope.getFontSize().value).isEqualTo(45)
+ }
+ }
+
+ private class TestAutoSize(private val testParam: Int) : AutoSize {
+ override fun FontSizeSearchScope.getFontSize(): TextUnit {
+ return if (!performLayoutAndGetOverflow(testParam.sp)) testParam.sp else 3.sp
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is TestAutoSize) return false
+
+ return testParam == other.testParam
+ }
+
+ override fun hashCode(): Int {
+ return testParam
+ }
+ }
+
+ private class AlwaysOverflows : FontSizeSearchScope {
+ override val density = 1f
+ override val fontScale = 1f
+
+ override fun performLayoutAndGetOverflow(fontSize: TextUnit): Boolean {
+ return true
+ }
+ }
+
+ private class NeverOverflows : FontSizeSearchScope {
+ override val density = 1f
+ override val fontScale = 1f
+
+ override fun performLayoutAndGetOverflow(fontSize: TextUnit): Boolean {
+ return false
+ }
+ }
+
+ private class OverflowsWhenFontSizeIsGreaterThan60px : FontSizeSearchScope {
+ override val density = 1f
+ override val fontScale = 1f
+
+ override fun performLayoutAndGetOverflow(fontSize: TextUnit): Boolean {
+ return fontSize.toPx() > 60
+ }
+ }
+}
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 9d07996..da1086f 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
@@ -172,7 +172,7 @@
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
) =
detectDragGestures(
- onDragStart = { change, _ -> onDragStart(change.position) },
+ onDragStart = { _, slopTriggerChange, _ -> onDragStart(slopTriggerChange.position) },
onDragEnd = { onDragEnd.invoke() },
onDragCancel = onDragCancel,
shouldAwaitTouchSlop = { true },
@@ -200,7 +200,8 @@
*
* @param onDragStart A lambda to be called when the drag gesture starts, it contains information
* about the last known [PointerInputChange] relative to the containing element and the post slop
- * delta.
+ * delta, slopTriggerChange. It also contains information about the down event where this gesture
+ * started and the overSlopOffset.
* @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
@@ -224,7 +225,10 @@
* @see detectDragGesturesAfterLongPress to detect gestures after long press
*/
internal suspend fun PointerInputScope.detectDragGestures(
- onDragStart: (change: PointerInputChange, initialDelta: Offset) -> Unit,
+ onDragStart:
+ (
+ down: PointerInputChange, slopTriggerChange: PointerInputChange, overSlopOffset: Offset
+ ) -> Unit,
onDragEnd: (change: PointerInputChange) -> Unit,
onDragCancel: () -> Unit,
shouldAwaitTouchSlop: () -> Boolean,
@@ -242,7 +246,6 @@
}
val down = awaitFirstDown(requireUnconsumed = false)
var drag: PointerInputChange?
- var initialDelta = Offset.Zero
overSlop = Offset.Zero
if (awaitTouchSlop) {
@@ -257,13 +260,12 @@
overSlop = over
}
} while (drag != null && !drag.isConsumed)
- initialDelta = overSlop
} else {
drag = initialDown
}
if (drag != null) {
- onDragStart.invoke(drag, initialDelta)
+ onDragStart.invoke(down, drag, overSlop)
onDrag(drag, overSlop)
val upEvent =
drag(
@@ -879,6 +881,7 @@
var currentDown = initialDown
val longPressTimeout = viewConfiguration.longPressTimeoutMillis
return try {
+ var deepPress = false
// wait for first tap up or long press
withTimeout(longPressTimeout) {
var finished = false
@@ -897,6 +900,11 @@
finished = true // Canceled
}
+ if (event.isDeepPress) {
+ deepPress = true
+ finished = true
+ }
+
// Check for cancel by position consumption. We can look on the Final pass of
// the existing pointer event because it comes after the Main pass we checked
// above.
@@ -919,7 +927,13 @@
}
}
}
- null
+ // If we finished early because of a deep press, return the relevant change as this counts
+ // as a long press
+ if (deepPress) {
+ longPress ?: initialDown
+ } else {
+ null
+ }
} catch (_: PointerEventTimeoutCancellationException) {
longPress ?: initialDown
}
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 f0490ff..82ad253 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
@@ -48,7 +48,6 @@
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.Velocity
import kotlin.coroutines.cancellation.CancellationException
-import kotlin.math.sign
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.coroutineScope
@@ -463,23 +462,27 @@
// 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)) {
+
+ val onDragStart:
+ (
+ down: PointerInputChange,
+ slopTriggerChange: PointerInputChange,
+ postSlopOffset: Offset
+ ) -> Unit =
+ { down, slopTriggerChange, postSlopOffset ->
+ if (canDrag.invoke(down)) {
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))
+ velocityTracker.addPointerInputChange(down)
+ val dragStartedOffset = slopTriggerChange.position - postSlopOffset
+ // the drag start event offset is the down event + touch slop value
+ // or in this case the event that triggered the touch slop minus
+ // the post slop offset
+ channel?.trySend(DragStarted(dragStartedOffset))
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt
index f875f45..e8f8b86 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt
@@ -111,31 +111,39 @@
}
}
if (onPress !== NoPressGesture) launch { pressScope.onPress(down.position) }
- val longPressTimeout =
- onLongPress?.let { viewConfiguration.longPressTimeoutMillis } ?: (Long.MAX_VALUE / 2)
- var upOrCancel: PointerInputChange? = null
- var cancelOrReleaseJob: Job?
- try {
- // wait for first tap up or long press
- upOrCancel = withTimeout(longPressTimeout) { waitForUpOrCancellation() }
- if (upOrCancel == null) {
- cancelOrReleaseJob =
- launch(start = coroutineStartForCurrentDispatchBehavior) {
- awaitResetOrSkip()
- // tap-up was canceled
- pressScope.cancel()
+ val upOrCancel: PointerInputChange?
+ val cancelOrReleaseJob: Job?
+
+ // wait for first tap up or long press
+ if (onLongPress == null) {
+ upOrCancel = waitForUpOrCancellation()
+ } else {
+ upOrCancel =
+ when (val longPressResult = waitForLongPress()) {
+ LongPressResult.Success -> {
+ onLongPress.invoke(down.position)
+ consumeUntilUp()
+ launch(start = coroutineStartForCurrentDispatchBehavior) {
+ awaitResetOrSkip()
+ pressScope.release()
+ }
+ // End the current gesture
+ return@awaitEachGesture
}
- } else {
- upOrCancel.consume()
- cancelOrReleaseJob =
- launch(start = coroutineStartForCurrentDispatchBehavior) {
- awaitResetOrSkip()
- pressScope.release()
- }
- }
- } catch (_: PointerEventTimeoutCancellationException) {
- onLongPress?.invoke(down.position)
- consumeUntilUp()
+ is LongPressResult.Released -> longPressResult.finalUpChange
+ is LongPressResult.Canceled -> null
+ }
+ }
+
+ if (upOrCancel == null) {
+ cancelOrReleaseJob =
+ launch(start = coroutineStartForCurrentDispatchBehavior) {
+ awaitResetOrSkip()
+ // tap-up was canceled
+ pressScope.cancel()
+ }
+ } else {
+ upOrCancel.consume()
cancelOrReleaseJob =
launch(start = coroutineStartForCurrentDispatchBehavior) {
awaitResetOrSkip()
@@ -157,45 +165,54 @@
// Second tap down detected
resetJob =
launch(start = coroutineStartForCurrentDispatchBehavior) {
- cancelOrReleaseJob?.join()
+ cancelOrReleaseJob.join()
pressScope.reset()
}
if (onPress !== NoPressGesture) {
launch { pressScope.onPress(secondDown.position) }
}
- try {
- // Might have a long second press as the second tap
- withTimeout(longPressTimeout) {
- val secondUp = waitForUpOrCancellation()
- if (secondUp != null) {
- secondUp.consume()
- launch(start = coroutineStartForCurrentDispatchBehavior) {
- awaitResetOrSkip()
- pressScope.release()
+ // Might have a long second press as the second tap
+ val secondUp =
+ if (onLongPress == null) {
+ waitForUpOrCancellation()
+ } else {
+ when (val longPressResult = waitForLongPress()) {
+ LongPressResult.Success -> {
+ // The first tap was valid, but the second tap is a long press -
+ // we
+ // intentionally do not invoke onClick() for the first tap,
+ // since the 'main'
+ // gesture here is a long press, which canceled the double tap
+ // / tap.
+
+ // notify for the long press
+ onLongPress.invoke(secondDown.position)
+ consumeUntilUp()
+
+ launch(start = coroutineStartForCurrentDispatchBehavior) {
+ awaitResetOrSkip()
+ pressScope.release()
+ }
+ return@awaitEachGesture
}
- onDoubleTap(secondUp.position)
- } else {
- launch(start = coroutineStartForCurrentDispatchBehavior) {
- awaitResetOrSkip()
- pressScope.cancel()
- }
- onTap?.invoke(upOrCancel.position)
+ is LongPressResult.Released -> longPressResult.finalUpChange
+ is LongPressResult.Canceled -> null
}
}
- } catch (e: PointerEventTimeoutCancellationException) {
- // The first tap was valid, but the second tap is a long press - we
- // intentionally do not invoke onClick() for the first tap, since the 'main'
- // gesture here is a long press, which cancelled the double tap / tap.
-
- // notify for the long press
- onLongPress?.invoke(secondDown.position)
- consumeUntilUp()
-
+ if (secondUp != null) {
+ secondUp.consume()
launch(start = coroutineStartForCurrentDispatchBehavior) {
awaitResetOrSkip()
pressScope.release()
}
+ onDoubleTap(secondUp.position)
+ } else {
+ launch(start = coroutineStartForCurrentDispatchBehavior) {
+ awaitResetOrSkip()
+ pressScope.cancel()
+ }
+ onTap?.invoke(upOrCancel.position)
}
}
}
@@ -318,6 +335,12 @@
waitForUpOrCancellation(PointerEventPass.Main)
/**
+ * Whether the event is considered a deep press, and should trigger long click before the timeout
+ * has been reached.
+ */
+internal expect val PointerEvent.isDeepPress: Boolean
+
+/**
* Reads events in the given [pass] until all pointers are up or the gesture was canceled. The
* gesture is considered canceled when a pointer leaves the event region, a position change has been
* consumed or a pointer down change event was already consumed in the given pass. If the gesture
@@ -348,6 +371,66 @@
}
}
+/**
+ * Reads events in the given [pass] until all pointers are up or the gesture was canceled. The
+ * gesture is considered canceled when a pointer leaves the event region, a position change has been
+ * consumed or a pointer down change event was already consumed in the given pass. If the gesture
+ * was not canceled, the final up change is returned or `null` if the event was canceled.
+ */
+private suspend fun AwaitPointerEventScope.waitForLongPress(
+ pass: PointerEventPass = PointerEventPass.Main
+): LongPressResult {
+ var result: LongPressResult = LongPressResult.Canceled
+ try {
+ withTimeout(viewConfiguration.longPressTimeoutMillis) {
+ while (true) {
+ val event = awaitPointerEvent(pass)
+ if (event.changes.fastAll { it.changedToUp() }) {
+ // All pointers are up
+ result = LongPressResult.Released(event.changes[0])
+ break
+ }
+
+ if (event.isDeepPress) {
+ result = LongPressResult.Success
+ break
+ }
+
+ if (
+ event.changes.fastAny {
+ it.isConsumed || it.isOutOfBounds(size, extendedTouchPadding)
+ }
+ ) {
+ result = LongPressResult.Canceled
+ break
+ }
+
+ // Check for cancel by position consumption. We can look on the Final pass of the
+ // existing pointer event because it comes after the pass we checked above.
+ val consumeCheck = awaitPointerEvent(PointerEventPass.Final)
+ if (consumeCheck.changes.fastAny { it.isConsumed }) {
+ result = LongPressResult.Canceled
+ break
+ }
+ }
+ }
+ } catch (_: PointerEventTimeoutCancellationException) {
+ return LongPressResult.Success
+ }
+ return result
+}
+
+private sealed class LongPressResult {
+ /** Long press was triggered */
+ object Success : LongPressResult()
+
+ /** All pointers were released without long press being triggered */
+ class Released(val finalUpChange: PointerInputChange) : LongPressResult()
+
+ /** The gesture was canceled */
+ object Canceled : LongPressResult()
+}
+
@Retention(AnnotationRetention.BINARY)
@RequiresOptIn("This API feature-flags new behavior and will be removed in the future.")
annotation class ExperimentalTapGestureDetectorBehaviorApi
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListAnimateScrollScope.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListAnimateScrollScope.kt
index 9b02c77..f9afcf1 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListAnimateScrollScope.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListAnimateScrollScope.kt
@@ -55,14 +55,14 @@
override fun calculateDistanceTo(targetIndex: Int, targetOffset: Int): Int {
val layoutInfo = state.layoutInfo
if (layoutInfo.visibleItemsInfo.isEmpty()) return 0
- val visibleItem =
- layoutInfo.visibleItemsInfo.fastFirstOrNull { it.index == targetIndex }
- return if (visibleItem == null) {
+ return if (targetIndex !in firstVisibleItemIndex..lastVisibleItemIndex) {
val averageSize = calculateVisibleItemsAverageSize(layoutInfo)
val indexesDiff = targetIndex - firstVisibleItemIndex
(averageSize * indexesDiff) - firstVisibleItemScrollOffset
} else {
- visibleItem.offset
+ val visibleItem =
+ layoutInfo.visibleItemsInfo.fastFirstOrNull { it.index == targetIndex }
+ visibleItem?.offset ?: 0
} + targetOffset
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
index f03d8b3..423483f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
@@ -422,10 +422,17 @@
consumedScroll = consumedScroll,
measureResult =
layout(layoutWidth, layoutHeight) {
- // place normal items
- positionedItems.fastForEach { it.place(this, isLookingAhead) }
- // stickingItems should be placed after all other items
- stickingItems.fastForEach { it.place(this, isLookingAhead) }
+ // Tagging as motion frame of reference placement, meaning the placement
+ // contains scrolling. This allows the consumer of this placement offset to
+ // differentiate this offset vs. offsets from structural changes. Generally
+ // speaking, this signals a preference to directly apply changes rather than
+ // animating, to avoid a chasing effect to scrolling.
+ withMotionFrameOfReferencePlacement {
+ // place normal items
+ positionedItems.fastForEach { it.place(this, isLookingAhead) }
+ // stickingItems should be placed after all other items
+ stickingItems.fastForEach { it.place(this, isLookingAhead) }
+ }
// we attach it during the placement so LazyListState can trigger re-placement
placementScopeInvalidator.attachToScope()
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateScrollScope.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateScrollScope.kt
index 46353b5..bf90c5e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateScrollScope.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateScrollScope.kt
@@ -53,10 +53,7 @@
override fun calculateDistanceTo(targetIndex: Int, targetOffset: Int): Int {
val layoutInfo = state.layoutInfo
if (layoutInfo.visibleItemsInfo.isEmpty()) return 0
- val visibleItem =
- layoutInfo.visibleItemsInfo.fastFirstOrNull { it.index == targetIndex }
-
- return if (visibleItem == null) {
+ return if (targetIndex !in firstVisibleItemIndex..lastVisibleItemIndex) {
val slotsPerLine = state.slotsPerLine
val averageLineMainAxisSize = calculateLineAverageMainAxisSize(layoutInfo)
val before = targetIndex < firstVisibleItemIndex
@@ -65,11 +62,13 @@
(slotsPerLine - 1) * if (before) -1 else 1) / slotsPerLine
(averageLineMainAxisSize * linesDiff) - firstVisibleItemScrollOffset
} else {
+ val visibleItem =
+ layoutInfo.visibleItemsInfo.fastFirstOrNull { it.index == targetIndex }
if (layoutInfo.orientation == Orientation.Vertical) {
- visibleItem.offset.y
+ visibleItem?.offset?.y
} else {
- visibleItem.offset.x
- }
+ visibleItem?.offset?.x
+ } ?: 0
} + targetOffset
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
index 22d00c9..66ed93c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
@@ -390,8 +390,15 @@
consumedScroll = consumedScroll,
measureResult =
layout(layoutWidth, layoutHeight) {
- positionedItems.fastForEach { it.place(this) }
- stickingItems.fastForEach { it.place(this) }
+ // Tagging as motion frame of reference placement, meaning the placement
+ // contains scrolling. This allows the consumer of this placement offset to
+ // differentiate this offset vs. offsets from structural changes. Generally
+ // speaking, this signals a preference to directly apply changes rather than
+ // animating, to avoid a chasing effect to scrolling.
+ withMotionFrameOfReferencePlacement {
+ positionedItems.fastForEach { it.place(this) }
+ stickingItems.fastForEach { it.place(this) }
+ }
// we attach it during the placement so LazyGridState can trigger re-placement
placementScopeInvalidator.attachToScope()
},
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutAnimateScroll.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutAnimateScroll.kt
index c889773..fbedddb 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutAnimateScroll.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutAnimateScroll.kt
@@ -171,6 +171,7 @@
var prevValue = 0f
anim.animateTo(target, sequentialAnimation = (anim.velocity != 0f)) {
// If we haven't found the item yet, check if it's visible.
+ debugLog { "firstVisibleItemIndex=$firstVisibleItemIndex" }
if (!isItemVisible(index)) {
// Springs can overshoot their target, clamp to the desired range
val coercedValue =
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 15bba67..608b82b 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
@@ -897,8 +897,15 @@
consumedScroll = consumedScroll,
measureResult =
layout(layoutWidth, layoutHeight) {
- positionedItems.fastForEach { item ->
- item.place(scope = this, context = this@measure)
+ // Tagging as motion frame of reference placement, meaning the placement
+ // contains scrolling. This allows the consumer of this placement offset to
+ // differentiate this offset vs. offsets from structural changes. Generally
+ // speaking, this signals a preference to directly apply changes rather than
+ // animating, to avoid a chasing effect to scrolling.
+ withMotionFrameOfReferencePlacement {
+ positionedItems.fastForEach { item ->
+ item.place(scope = this, context = this@measure)
+ }
}
// we attach it during the placement so LazyStaggeredGridState can trigger
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasure.kt
index 1333583..a6f83cd 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasure.kt
@@ -465,7 +465,14 @@
firstVisiblePageScrollOffset = currentFirstPageScrollOffset,
measureResult =
layout(layoutWidth, layoutHeight) {
- positionedPages.fastForEach { it.place(this) }
+ // Tagging as motion frame of reference placement, meaning the placement
+ // contains scrolling. This allows the consumer of this placement offset to
+ // differentiate this offset vs. offsets from structural changes. Generally
+ // speaking, this signals a preference to directly apply changes rather than
+ // animating, to avoid a chasing effect to scrolling.
+ withMotionFrameOfReferencePlacement {
+ positionedPages.fastForEach { it.place(this) }
+ }
// we attach it during the placement so PagerState can trigger re-placement
placementScopeInvalidator.attachToScope()
},
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/AutoSize.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/AutoSize.kt
new file mode 100644
index 0000000..c900e10
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/AutoSize.kt
@@ -0,0 +1,225 @@
+/*
+ * 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.foundation.text
+
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.TextUnitType
+import androidx.compose.ui.unit.sp
+import kotlin.math.floor
+
+/** Interface used by Text composables to automatically size their text. */
+internal interface AutoSize {
+ /**
+ * Calculates font size. Use utility function [FontSizeSearchScope.performLayoutAndGetOverflow]
+ * to lay out the text and check if it overflows. The expectation is that
+ * implementation-specific constraints should be used in unison with
+ * [FontSizeSearchScope.performLayoutAndGetOverflow] to determine a suitable font size to be
+ * used.
+ *
+ * @return The derived optimal font size.
+ * @see [FontSizeSearchScope.performLayoutAndGetOverflow]
+ */
+ // TODO(b/362904946): Add sample
+ fun FontSizeSearchScope.getFontSize(): TextUnit
+
+ /**
+ * Require equals() to be implemented for performance purposes. Using a data class is
+ * sufficient. Singletons may implement this function with referential equality (`this ===
+ * other`). Instances with no properties may implement this function by checking the type of the
+ * other object.
+ *
+ * @return true if both AutoSize instances are structurally identical.
+ */
+ override fun equals(other: Any?): Boolean
+
+ companion object {
+ /**
+ * Automatically size the text to attempt to fit its container. This uses a
+ * step-based/granular implementation where potential font sizes are uniformly spread out
+ * between [minFontSize] and [maxFontSize]. [stepSize] is the smallest difference between
+ * two distinct font sizes. e.g. if `minFontSize = 1.sp`, `maxFontSize = 2.sp` and `stepSize
+ * = 0.5.sp`, the potential font sizes are `1.sp`, `1.5.sp`, and `2.sp`. In cases where
+ * [stepSize] is strictly greater than (not equal to) the difference between [minFontSize]
+ * and [maxFontSize], the only potential font size is [minFontSize].
+ *
+ * Both or neither [minFontSize] and [maxFontSize] must be declared.
+ *
+ * @param minFontSize The smallest potential font size of the text. Default = 12.sp. This
+ * must be smaller than [maxFontSize]; an [IllegalArgumentException] will be thrown
+ * otherwise.
+ * @param maxFontSize The largest potential font size of the text. Default = 112.sp. This
+ * must be larger than [minFontSize]; an [IllegalArgumentException] will be thrown
+ * otherwise.
+ * @param stepSize The smallest difference between potential font sizes. Specifically, every
+ * font size, when subtracted by [minFontSize], is divisible by [stepSize]. Default =
+ * 0.25.sp. This must not be less than `0.0001f.sp`; an [IllegalArgumentException] will be
+ * thrown otherwise.
+ * @return AutoSize instance with the step-based configuration. Using this in a compatible
+ * composable will cause its text to be sized as above.
+ */
+ fun StepBased(
+ minFontSize: TextUnit,
+ maxFontSize: TextUnit,
+ stepSize: TextUnit = 0.25.sp
+ ): AutoSize {
+ return AutoSizeStepBased(minFontSize, maxFontSize, stepSize)
+ }
+
+ /**
+ * Automatically size the text to attempt to fit its container. This uses a
+ * step-based/granular implementation where potential font sizes are uniformly spread out
+ * between `minFontSize` and `maxFontSize`. [stepSize] is the smallest difference between
+ * two distinct font sizes. e.g. if `minFontSize = 1.sp`, `maxFontSize = 2.sp` and `stepSize
+ * = 0.5.sp`, the potential font sizes are `1.sp`, `1.5.sp`, and `2.sp`. In cases where
+ * [stepSize] is strictly greater than (not equal to) the difference between `minFontSize`
+ * and `maxFontSize`, the only potential font size is `minFontSize`.
+ *
+ * Both or neither `minFontSize` and `maxFontSize` must be declared.
+ *
+ * @param stepSize The smallest difference between potential font sizes. Specifically, every
+ * font size, when subtracted by `minFontSize` (`12.sp`), is divisible by [stepSize].
+ * Default = 0.25.sp. [stepSize] must not be less than `0.0001f.sp`, an
+ * [IllegalArgumentException] will be thrown otherwise.
+ * @return AutoSize instance with the step-based configuration. Using this in a compatible
+ * composable will cause its text to be sized as above.
+ */
+ fun StepBased(stepSize: TextUnit = 0.25.sp): AutoSize {
+ return AutoSizeStepBased(minFontSize = 12.sp, maxFontSize = 112.sp, stepSize = stepSize)
+ }
+ }
+}
+
+/**
+ * This interface is used by classes responsible for laying out text. Layout will be performed here
+ * alongside logic that checks if the text overflows.
+ *
+ * These methods are used by [AutoSize] in the [AutoSize.getFontSize] method, where developers can
+ * lay out text with different font sizes and do certain logic depending on whether or not the text
+ * overflows.
+ *
+ * This may be implemented in unit tests when testing [AutoSize.getFontSize] to see if the method
+ * works as intended.
+ */
+internal interface FontSizeSearchScope : Density {
+ /**
+ * Lays out the text with the given font size.
+ *
+ * @return true if the text overflows.
+ */
+ fun performLayoutAndGetOverflow(fontSize: TextUnit): Boolean
+}
+
+private class AutoSizeStepBased(
+ private var minFontSize: TextUnit,
+ private val maxFontSize: TextUnit,
+ private val stepSize: TextUnit
+) : AutoSize {
+ init {
+ // Checks for validity of AutoSize instance
+ // Unspecified check
+ if (minFontSize == TextUnit.Unspecified) {
+ throw IllegalArgumentException(
+ "AutoSize.StepBased: TextUnit.Unspecified is not a valid value for minFontSize. " +
+ "Try using other values e.g. 10.sp"
+ )
+ }
+ if (maxFontSize == TextUnit.Unspecified) {
+ throw IllegalArgumentException(
+ "AutoSize.StepBased: TextUnit.Unspecified is not a valid value for maxFontSize. " +
+ "Try using other values e.g. 100.sp"
+ )
+ }
+ if (stepSize == TextUnit.Unspecified) {
+ throw IllegalArgumentException(
+ "AutoSize.StepBased: TextUnit.Unspecified is not a valid value for stepSize. " +
+ "Try using other values e.g. 0.25.sp"
+ )
+ }
+
+ // minFontSize maxFontSize comparison check
+ if (minFontSize.type == maxFontSize.type && minFontSize > maxFontSize) {
+ minFontSize = maxFontSize
+ }
+
+ // check if stepSize is too small
+ if (stepSize.type == TextUnitType.Sp && stepSize < 0.0001f.sp) {
+ throw IllegalArgumentException(
+ "AutoSize.StepBased: stepSize must be greater than or equal to 0.0001f.sp"
+ )
+ }
+
+ // check if minFontSize or maxFontSize are negative
+ if (minFontSize.value < 0) {
+ throw IllegalArgumentException("AutoSize.StepBased: minFontSize must not be negative")
+ }
+ if (maxFontSize.value < 0) {
+ throw IllegalArgumentException("AutoSize.StepBased: maxFontSize must not be negative")
+ }
+ }
+
+ override fun FontSizeSearchScope.getFontSize(): TextUnit {
+ val stepSize = stepSize.toPx()
+ val smallest = minFontSize.toPx()
+ val largest = maxFontSize.toPx()
+ var min = smallest
+ var max = largest
+
+ var current = (min + max) / 2
+
+ while ((max - min) >= stepSize) {
+ // overflow indicates that whole text doesn't fit
+ if (performLayoutAndGetOverflow(current.toSp())) {
+ max = current
+ } else {
+ min = current
+ }
+ current = (min + max) / 2
+ }
+ // used size minus minFontSize must be divisible by stepSize
+ current = (floor((current - smallest) / stepSize) * stepSize + smallest)
+
+ // try the next size up and see if it fits
+ if (
+ (current + stepSize) <= largest &&
+ !performLayoutAndGetOverflow((current + stepSize).toSp())
+ ) {
+ current += stepSize
+ }
+
+ return current.toSp()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (other === this) return true
+ if (other == null) return false
+ if (other !is AutoSizeStepBased) return false
+
+ if (other.minFontSize != minFontSize) return false
+ if (other.maxFontSize != maxFontSize) return false
+ if (other.stepSize != stepSize) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = minFontSize.hashCode()
+ result = 31 * result + maxFontSize.hashCode()
+ result = 31 * result + stepSize.hashCode()
+ return result
+ }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextDelegate.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextDelegate.kt
index b3125c6..efab65a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextDelegate.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextDelegate.kt
@@ -206,7 +206,7 @@
),
// This is a fallback behavior for ellipsis. Native
maxLines = finalMaxLines,
- ellipsis = overflow == TextOverflow.Ellipsis
+ overflow = overflow
)
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtils.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtils.kt
index 3f475b2..d9dff5d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtils.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtils.kt
@@ -41,7 +41,7 @@
overflow: TextOverflow,
maxIntrinsicWidth: Float
): Int {
- val widthMatters = softWrap || overflow == TextOverflow.Ellipsis
+ val widthMatters = softWrap || overflow.isEllipsis
val maxWidth =
if (widthMatters && constraints.hasBoundedWidth) {
constraints.maxWidth
@@ -81,6 +81,13 @@
// AA…
// Here we assume there won't be any '\n' character when softWrap is false. And make
// maxLines 1 to implement the similar behavior.
- val overwriteMaxLines = !softWrap && overflow == TextOverflow.Ellipsis
+ val overwriteMaxLines = !softWrap && overflow.isEllipsis
return if (overwriteMaxLines) 1 else maxLinesIn.coerceAtLeast(1)
}
+
+internal val TextOverflow.isEllipsis: Boolean
+ get() {
+ return this == TextOverflow.Ellipsis ||
+ this == TextOverflow.StartEllipsis ||
+ this == TextOverflow.MiddleEllipsis
+ }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/MinLinesConstrainer.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/MinLinesConstrainer.kt
index d3d5f65..1ecfcd1 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/MinLinesConstrainer.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/MinLinesConstrainer.kt
@@ -20,6 +20,7 @@
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.resolveDefaults
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
@@ -103,7 +104,7 @@
density = density,
fontFamilyResolver = fontFamilyResolver,
maxLines = 1,
- ellipsis = false
+ overflow = TextOverflow.Clip
)
.height
@@ -115,7 +116,7 @@
density = density,
fontFamilyResolver = fontFamilyResolver,
maxLines = 2,
- ellipsis = false
+ overflow = TextOverflow.Clip
)
.height
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCache.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCache.kt
index fe403c3..aaa4e25 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCache.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCache.kt
@@ -259,7 +259,7 @@
),
// This is a fallback behavior for ellipsis. Native
maxLines = finalMaxLines(softWrap, overflow, maxLines),
- ellipsis = overflow == TextOverflow.Ellipsis
+ overflow = overflow
)
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCache.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCache.kt
index 40a4249..f134d1b2 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCache.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCache.kt
@@ -249,7 +249,7 @@
),
// This is a fallback behavior for ellipsis. Native
maxLines = finalMaxLines(softWrap, overflow, maxLines),
- ellipsis = overflow == TextOverflow.Ellipsis
+ overflow = overflow
)
}
@@ -338,7 +338,7 @@
),
finalConstraints,
maxLines,
- overflow == TextOverflow.Ellipsis
+ overflow
),
layoutSize
)
diff --git a/compose/foundation/foundation/src/commonStubsMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.commonStubs.kt b/compose/foundation/foundation/src/commonStubsMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.commonStubs.kt
new file mode 100644
index 0000000..253dfeb
--- /dev/null
+++ b/compose/foundation/foundation/src/commonStubsMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.commonStubs.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.foundation.gestures
+
+import androidx.compose.ui.input.pointer.PointerEvent
+
+internal actual val PointerEvent.isDeepPress: Boolean
+ get() = false
diff --git a/compose/integration-tests/demos/common/build.gradle b/compose/integration-tests/demos/common/build.gradle
index cf434ee..34c02706 100644
--- a/compose/integration-tests/demos/common/build.gradle
+++ b/compose/integration-tests/demos/common/build.gradle
@@ -26,7 +26,7 @@
api("androidx.activity:activity:1.2.0")
api("androidx.fragment:fragment-ktx:1.3.6")
- implementation(projectOrArtifact(":compose:runtime:runtime"))
+ implementation(project(":compose:runtime:runtime"))
}
android {
diff --git a/compose/material/material-navigation/build.gradle b/compose/material/material-navigation/build.gradle
index 4cf82b4..5cbd3bd 100644
--- a/compose/material/material-navigation/build.gradle
+++ b/compose/material/material-navigation/build.gradle
@@ -48,7 +48,7 @@
inceptionYear = "2024"
description = "Compose Material integration with Navigation"
legacyDisableKotlinStrictApiMode = true
- samples(projectOrArtifact(":compose:material:material-navigation-samples"))
+ samples(project(":compose:material:material-navigation-samples"))
kotlinTarget = KotlinTarget.KOTLIN_1_9
}
diff --git a/compose/material3/adaptive/adaptive-layout/build.gradle b/compose/material3/adaptive/adaptive-layout/build.gradle
index f37fbdb..2f637a0 100644
--- a/compose/material3/adaptive/adaptive-layout/build.gradle
+++ b/compose/material3/adaptive/adaptive-layout/build.gradle
@@ -111,10 +111,6 @@
samples(project(":compose:material3:adaptive:adaptive-samples"))
}
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
-}
-
// Screenshot tests related setup
android {
compileSdk 35
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneMarginsTest.kt b/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneMarginsTest.kt
new file mode 100644
index 0000000..cfdc5f5
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneMarginsTest.kt
@@ -0,0 +1,459 @@
+/*
+ * 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.material3.adaptive.layout
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.layout.Ruler
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class PaneMarginsModifierTest {
+ @Test
+ fun unspecifiedPaneMargins_alwaysUseMeasuredValue() {
+ assertThat(with(PaneMargins.Unspecified) { MockPlacementScope().getPaneLeft(50) })
+ .isEqualTo(50)
+ assertThat(with(PaneMargins.Unspecified) { MockPlacementScope().getPaneTop(70) })
+ .isEqualTo(70)
+ assertThat(
+ with(PaneMargins.Unspecified) {
+ MockPlacementScope().getPaneRight(30, MockLayoutWidth)
+ }
+ )
+ .isEqualTo(30)
+ assertThat(
+ with(PaneMargins.Unspecified) {
+ MockPlacementScope().getPaneBottom(60, MockLayoutHeight)
+ }
+ )
+ .isEqualTo(60)
+ }
+
+ @Test
+ fun getPaneTop_noMarginsSet_useMeasuredTop() {
+ val mockPaneMargins =
+ PaneMarginsImpl(PaddingValues(), emptyList(), MockDensity, MockLayoutDirection)
+ assertThat(with(mockPaneMargins) { MockPlacementScope().getPaneTop(50) }).isEqualTo(50)
+ }
+
+ @Test
+ fun getPaneTop_noWindowInsets_useFixedMargins() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(0.dp, 100.dp, 0.dp, 0.dp),
+ emptyList(),
+ MockDensity,
+ MockLayoutDirection
+ )
+ assertThat(with(mockPaneMargins) { MockPlacementScope().getPaneTop(0) }).isEqualTo(100)
+ }
+
+ @Test
+ fun getPaneTop_multipleWindowInsets_useLargestOne() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(),
+ listOf(MockWindowInsetRulers1, MockWindowInsetRulers2, MockWindowInsetRulers3),
+ MockDensity,
+ MockLayoutDirection
+ )
+ assertThat(
+ with(mockPaneMargins) {
+ MockPlacementScope(mockInset1Top = 30, mockInset2Top = 60, mockInset3Top = 10)
+ .getPaneTop(0)
+ }
+ )
+ .isEqualTo(60)
+ }
+
+ @Test
+ fun getPaneTop_withFixedMarginsAndWindowInsets_useLargestOne() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(0.dp, 100.dp, 0.dp, 0.dp),
+ listOf(MockWindowInsetRulers1, MockWindowInsetRulers2, MockWindowInsetRulers3),
+ MockDensity,
+ MockLayoutDirection
+ )
+ assertThat(
+ with(mockPaneMargins) {
+ MockPlacementScope(mockInset1Top = 30, mockInset2Top = 60, mockInset3Top = 10)
+ .getPaneTop(0)
+ }
+ )
+ .isEqualTo(100)
+ }
+
+ @Test
+ fun getPaneTop_whenMeasuredTopIsLarger_useMeasuredTop() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(0.dp, 100.dp, 0.dp, 0.dp),
+ listOf(MockWindowInsetRulers1, MockWindowInsetRulers2, MockWindowInsetRulers3),
+ MockDensity,
+ MockLayoutDirection
+ )
+ assertThat(
+ with(mockPaneMargins) {
+ MockPlacementScope(mockInset1Top = 30, mockInset2Top = 60, mockInset3Top = 10)
+ .getPaneTop(140)
+ }
+ )
+ .isEqualTo(140)
+ }
+
+ @Test
+ fun getPaneBottom_noMarginsSet_useMeasuredBottom() {
+ val mockPaneMargins =
+ PaneMarginsImpl(PaddingValues(), emptyList(), MockDensity, MockLayoutDirection)
+ assertThat(
+ with(mockPaneMargins) { MockPlacementScope().getPaneBottom(850, MockLayoutHeight) }
+ )
+ .isEqualTo(850)
+ }
+
+ @Test
+ fun getPaneBottom_noWindowInsets_useFixedMargins() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(0.dp, 0.dp, 0.dp, 100.dp),
+ emptyList(),
+ MockDensity,
+ MockLayoutDirection
+ )
+ assertThat(
+ with(mockPaneMargins) { MockPlacementScope().getPaneBottom(1024, MockLayoutHeight) }
+ )
+ .isEqualTo(MockLayoutHeight - 100)
+ }
+
+ @Test
+ fun getPaneBottom_multipleWindowInsets_useSmallestOne() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(),
+ listOf(MockWindowInsetRulers1, MockWindowInsetRulers2, MockWindowInsetRulers3),
+ MockDensity,
+ MockLayoutDirection
+ )
+ assertThat(
+ with(mockPaneMargins) {
+ MockPlacementScope(
+ mockInset1Bottom = 930,
+ mockInset2Bottom = 960,
+ mockInset3Bottom = 910
+ )
+ .getPaneBottom(1024, MockLayoutHeight)
+ }
+ )
+ .isEqualTo(910)
+ }
+
+ @Test
+ fun getPaneBottom_withFixedMarginsAndWindowInsets_useSmallestOne() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(0.dp, 0.dp, 0.dp, 200.dp),
+ listOf(MockWindowInsetRulers1, MockWindowInsetRulers2, MockWindowInsetRulers3),
+ MockDensity,
+ MockLayoutDirection
+ )
+ assertThat(
+ with(mockPaneMargins) {
+ MockPlacementScope(
+ mockInset1Bottom = 930,
+ mockInset2Bottom = 960,
+ mockInset3Bottom = 910
+ )
+ .getPaneBottom(1024, MockLayoutHeight)
+ }
+ )
+ .isEqualTo(MockLayoutHeight - 200)
+ }
+
+ @Test
+ fun getPaneBottom_whenMeasuredBottomIsSmaller_useMeasuredBottom() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(0.dp, 0.dp, 0.dp, 200.dp),
+ listOf(MockWindowInsetRulers1, MockWindowInsetRulers2, MockWindowInsetRulers3),
+ MockDensity,
+ MockLayoutDirection
+ )
+ assertThat(
+ with(mockPaneMargins) {
+ MockPlacementScope(
+ mockInset1Bottom = 930,
+ mockInset2Bottom = 960,
+ mockInset3Bottom = 910
+ )
+ .getPaneBottom(800, MockLayoutHeight)
+ }
+ )
+ .isEqualTo(800)
+ }
+
+ @Test
+ fun getPaneLeft_noMarginsSet_useMeasuredLeft() {
+ val mockPaneMargins =
+ PaneMarginsImpl(PaddingValues(), emptyList(), MockDensity, MockLayoutDirection)
+ assertThat(with(mockPaneMargins) { MockPlacementScope().getPaneLeft(50) }).isEqualTo(50)
+ }
+
+ @Test
+ fun getPaneLeft_noWindowInsets_useFixedMargins() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(100.dp, 0.dp, 0.dp, 0.dp),
+ emptyList(),
+ MockDensity,
+ MockLayoutDirection
+ )
+ assertThat(with(mockPaneMargins) { MockPlacementScope().getPaneLeft(0) }).isEqualTo(100)
+ }
+
+ @Test
+ fun getPaneLeft_withRtlDirection_usePaddingEnd() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(0.dp, 0.dp, 110.dp, 0.dp),
+ emptyList(),
+ MockDensity,
+ LayoutDirection.Rtl
+ )
+ assertThat(with(mockPaneMargins) { MockPlacementScope().getPaneLeft(0) }).isEqualTo(110)
+ }
+
+ @Test
+ fun getPaneLeft_multipleWindowInsets_useLargestOne() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(),
+ listOf(MockWindowInsetRulers1, MockWindowInsetRulers2, MockWindowInsetRulers3),
+ MockDensity,
+ MockLayoutDirection
+ )
+ assertThat(
+ with(mockPaneMargins) {
+ MockPlacementScope(
+ mockInset1Left = 30,
+ mockInset2Left = 60,
+ mockInset3Left = 10
+ )
+ .getPaneLeft(0)
+ }
+ )
+ .isEqualTo(60)
+ }
+
+ @Test
+ fun getPaneLeft_withFixedMarginsAndWindowInsets_useLargestOne() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(100.dp, 0.dp, 0.dp, 0.dp),
+ listOf(MockWindowInsetRulers1, MockWindowInsetRulers2, MockWindowInsetRulers3),
+ MockDensity,
+ MockLayoutDirection
+ )
+ assertThat(
+ with(mockPaneMargins) {
+ MockPlacementScope(
+ mockInset1Left = 30,
+ mockInset2Left = 60,
+ mockInset3Left = 10
+ )
+ .getPaneLeft(0)
+ }
+ )
+ .isEqualTo(100)
+ }
+
+ @Test
+ fun getPaneLeft_whenMeasuredLeftIsLarger_useMeasuredLeft() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(100.dp, 0.dp, 0.dp, 0.dp),
+ listOf(MockWindowInsetRulers1, MockWindowInsetRulers2, MockWindowInsetRulers3),
+ MockDensity,
+ MockLayoutDirection
+ )
+ assertThat(
+ with(mockPaneMargins) {
+ MockPlacementScope(
+ mockInset1Left = 30,
+ mockInset2Left = 60,
+ mockInset3Left = 10
+ )
+ .getPaneLeft(140)
+ }
+ )
+ .isEqualTo(140)
+ }
+
+ @Test
+ fun getPaneRight_noMarginsSet_useMeasuredRight() {
+ val mockPaneMargins =
+ PaneMarginsImpl(PaddingValues(), emptyList(), MockDensity, MockLayoutDirection)
+ assertThat(
+ with(mockPaneMargins) { MockPlacementScope().getPaneRight(850, MockLayoutWidth) }
+ )
+ .isEqualTo(850)
+ }
+
+ @Test
+ fun getPaneRight_noWindowInsets_useFixedMargins() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(0.dp, 0.dp, 100.dp, 0.dp),
+ emptyList(),
+ MockDensity,
+ MockLayoutDirection
+ )
+ assertThat(
+ with(mockPaneMargins) { MockPlacementScope().getPaneRight(1280, MockLayoutWidth) }
+ )
+ .isEqualTo(MockLayoutWidth - 100)
+ }
+
+ @Test
+ fun getPaneRight_withRtlDirection_usePaddingStart() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(110.dp, 0.dp, 0.dp, 0.dp),
+ emptyList(),
+ MockDensity,
+ LayoutDirection.Rtl
+ )
+ assertThat(
+ with(mockPaneMargins) { MockPlacementScope().getPaneRight(1280, MockLayoutWidth) }
+ )
+ .isEqualTo(MockLayoutWidth - 110)
+ }
+
+ @Test
+ fun getPaneRight_multipleWindowInsets_useSmallestOne() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(),
+ listOf(MockWindowInsetRulers1, MockWindowInsetRulers2, MockWindowInsetRulers3),
+ MockDensity,
+ MockLayoutDirection
+ )
+ assertThat(
+ with(mockPaneMargins) {
+ MockPlacementScope(
+ mockInset1Right = 930,
+ mockInset2Right = 960,
+ mockInset3Right = 910
+ )
+ .getPaneRight(1280, MockLayoutWidth)
+ }
+ )
+ .isEqualTo(910)
+ }
+
+ @Test
+ fun getPaneRight_withFixedMarginsAndWindowInsets_useSmallestOne() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(0.dp, 0.dp, 200.dp, 0.dp),
+ listOf(MockWindowInsetRulers1, MockWindowInsetRulers2, MockWindowInsetRulers3),
+ MockDensity,
+ MockLayoutDirection
+ )
+ assertThat(
+ with(mockPaneMargins) {
+ MockPlacementScope(
+ mockInset1Right = 930,
+ mockInset2Right = 960,
+ mockInset3Right = 910
+ )
+ .getPaneRight(1280, MockLayoutWidth)
+ }
+ )
+ .isEqualTo(910)
+ }
+
+ @Test
+ fun getPaneRight_whenMeasuredRightIsSmaller_useMeasuredRight() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(0.dp, 0.dp, 200.dp, 0.dp),
+ listOf(MockWindowInsetRulers1, MockWindowInsetRulers2, MockWindowInsetRulers3),
+ MockDensity,
+ MockLayoutDirection
+ )
+ assertThat(
+ with(mockPaneMargins) {
+ MockPlacementScope(
+ mockInset1Right = 930,
+ mockInset2Right = 960,
+ mockInset3Right = 910
+ )
+ .getPaneRight(800, MockLayoutWidth)
+ }
+ )
+ .isEqualTo(800)
+ }
+}
+
+private val MockDensity = Density(1f)
+private val MockLayoutDirection = LayoutDirection.Ltr
+private const val MockLayoutWidth = 1280
+private const val MockLayoutHeight = 1024
+
+private val MockWindowInsetRulers1 = WindowInsetsRulers()
+private val MockWindowInsetRulers2 = WindowInsetsRulers()
+private val MockWindowInsetRulers3 = WindowInsetsRulers()
+
+private class MockPlacementScope(
+ val mockInset1Left: Int = 0,
+ val mockInset1Top: Int = 0,
+ val mockInset1Right: Int = 0,
+ val mockInset1Bottom: Int = 0,
+ val mockInset2Left: Int = 0,
+ val mockInset2Top: Int = 0,
+ val mockInset2Right: Int = 0,
+ val mockInset2Bottom: Int = 0,
+ val mockInset3Left: Int = 0,
+ val mockInset3Top: Int = 0,
+ val mockInset3Right: Int = 0,
+ val mockInset3Bottom: Int = 0,
+) : Placeable.PlacementScope() {
+ override val parentWidth = MockLayoutWidth
+ override val parentLayoutDirection = MockLayoutDirection
+
+ override fun Ruler.current(defaultValue: Float): Float =
+ when (this) {
+ MockWindowInsetRulers1.left -> mockInset1Left.toFloat()
+ MockWindowInsetRulers1.top -> mockInset1Top.toFloat()
+ MockWindowInsetRulers1.right -> mockInset1Right.toFloat()
+ MockWindowInsetRulers1.bottom -> mockInset1Bottom.toFloat()
+ MockWindowInsetRulers2.left -> mockInset2Left.toFloat()
+ MockWindowInsetRulers2.top -> mockInset2Top.toFloat()
+ MockWindowInsetRulers2.right -> mockInset2Right.toFloat()
+ MockWindowInsetRulers2.bottom -> mockInset2Bottom.toFloat()
+ MockWindowInsetRulers3.left -> mockInset3Left.toFloat()
+ MockWindowInsetRulers3.top -> mockInset3Top.toFloat()
+ MockWindowInsetRulers3.right -> mockInset3Right.toFloat()
+ MockWindowInsetRulers3.bottom -> mockInset3Bottom.toFloat()
+ else -> 0f
+ }
+}
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneMargins.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneMargins.kt
new file mode 100644
index 0000000..c6e6f11
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneMargins.kt
@@ -0,0 +1,182 @@
+/*
+ * 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.material3.adaptive.layout
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.HorizontalRuler
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.layout.VerticalRuler
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.ParentDataModifierNode
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
+import kotlin.math.roundToInt
+
+// TODO(conradchen): move the modifier declarations to PaneScaffoldPaneScope when we can publish it.
+/**
+ * This modifier specifies the associated pane's margins according to the provided
+ * [WindowInsetsRulers]. Note that if multiple window inset rulers are provided, the scaffold will
+ * decide the actual margins by taking the union of these insets - i.e. the one creating the largest
+ * margins will be used.
+ *
+ * @param windowInsets the window insets the pane wants to respect.
+ */
+@ExperimentalMaterial3AdaptiveApi
+@Composable
+internal fun Modifier.paneMargins(vararg windowInsets: WindowInsetsRulers) =
+ paneMargins(PaddingValues(), windowInsets.toList())
+
+// TODO(conradchen): move the modifier declarations to PaneScaffoldPaneScope when we can publish it.
+/**
+ * This modifier specifies the associated pane's margins according to specified fixed margins and
+ * the provided [WindowInsetsRulers], if any. Note that the scaffold will decide the actual margins
+ * by taking the union of the fixed margins and the provided insets - i.e. the one creating the
+ * largest margins will be used.
+ *
+ * @param fixedMargins fixed margins to use for the pane.
+ * @param windowInsets the window insets the pane wants to respect.
+ */
+@ExperimentalMaterial3AdaptiveApi
+@Composable
+internal fun Modifier.paneMargins(
+ fixedMargins: PaddingValues,
+ vararg windowInsets: WindowInsetsRulers
+) = paneMargins(fixedMargins, windowInsets.toList())
+
+@Composable
+private fun Modifier.paneMargins(
+ fixedMargins: PaddingValues,
+ windowInsets: List<WindowInsetsRulers>
+) =
+ this.then(
+ PaneMarginsElement(
+ PaneMarginsImpl(
+ fixedMargins,
+ windowInsets,
+ LocalDensity.current,
+ LocalLayoutDirection.current
+ )
+ )
+ )
+
+private data class PaneMarginsElement(val paneMargins: PaneMargins) :
+ ModifierNodeElement<PaneMarginsNode>() {
+ private val inspectorInfo = debugInspectorInfo {
+ name = "paneMargins"
+ properties["paneMargins"] = paneMargins
+ }
+
+ override fun create(): PaneMarginsNode {
+ return PaneMarginsNode(paneMargins)
+ }
+
+ override fun update(node: PaneMarginsNode) {
+ node.paneMargins = paneMargins
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ inspectorInfo()
+ }
+}
+
+private class PaneMarginsNode(var paneMargins: PaneMargins) :
+ ParentDataModifierNode, Modifier.Node() {
+ override fun Density.modifyParentData(parentData: Any?) =
+ ((parentData as? PaneScaffoldParentData) ?: PaneScaffoldParentData()).also {
+ it.paneMargins = paneMargins
+ }
+}
+
+@Immutable
+internal interface PaneMargins {
+ fun Placeable.PlacementScope.getPaneLeft(measuredLeft: Int) = measuredLeft
+
+ fun Placeable.PlacementScope.getPaneTop(measuredTop: Int) = measuredTop
+
+ fun Placeable.PlacementScope.getPaneRight(measuredRight: Int, parentRight: Int) = measuredRight
+
+ fun Placeable.PlacementScope.getPaneBottom(measuredBottom: Int, parentBottom: Int) =
+ measuredBottom
+
+ companion object {
+ val Unspecified = object : PaneMargins {}
+ }
+}
+
+@Immutable
+internal class PaneMarginsImpl(
+ fixedMargins: PaddingValues = PaddingValues(),
+ windowInsets: List<WindowInsetsRulers>,
+ density: Density,
+ layoutDirection: LayoutDirection
+) : PaneMargins {
+ private val fixedMarginLeft =
+ with(density) { fixedMargins.calculateLeftPadding(layoutDirection).roundToPx() }
+ private val fixedMarginTop = with(density) { fixedMargins.calculateTopPadding().roundToPx() }
+ private val fixedMarginRight =
+ with(density) { fixedMargins.calculateRightPadding(layoutDirection).roundToPx() }
+ private val fixedMarginBottom =
+ with(density) { fixedMargins.calculateBottomPadding().roundToPx() }
+ private val rulers = windowInsets
+
+ override fun Placeable.PlacementScope.getPaneLeft(measuredLeft: Int): Int =
+ maxOf(
+ measuredLeft,
+ fixedMarginLeft,
+ rulers.maxOfOrNull { it.left.current(0f).roundToInt() } ?: 0
+ )
+
+ override fun Placeable.PlacementScope.getPaneTop(measuredTop: Int): Int =
+ maxOf(
+ measuredTop,
+ fixedMarginTop,
+ rulers.maxOfOrNull { it.top.current(0f).roundToInt() } ?: 0
+ )
+
+ override fun Placeable.PlacementScope.getPaneRight(measuredRight: Int, parentRight: Int): Int =
+ minOf(
+ measuredRight,
+ parentRight - fixedMarginRight,
+ rulers.minOfOrNull { it.right.current(Float.MAX_VALUE).roundToInt() } ?: parentRight
+ )
+
+ override fun Placeable.PlacementScope.getPaneBottom(
+ measuredBottom: Int,
+ parentBottom: Int
+ ): Int =
+ minOf(
+ measuredBottom,
+ parentBottom - fixedMarginBottom,
+ rulers.minOfOrNull { it.bottom.current(Float.MAX_VALUE).roundToInt() } ?: parentBottom
+ )
+}
+
+// TODO(conradchen): Move to use the foundation definition when it's available
+internal class WindowInsetsRulers {
+ val left = VerticalRuler()
+ val top = HorizontalRuler()
+ val right = VerticalRuler()
+ val bottom = HorizontalRuler()
+}
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt
index 3fd930e..551f730 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt
@@ -192,5 +192,6 @@
internal data class PaneScaffoldParentData(
var preferredWidth: Float? = null,
+ var paneMargins: PaneMargins = PaneMargins.Unspecified,
var isAnimatedPane: Boolean = false
)
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
index bebc466..37f09c3 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
@@ -693,6 +693,8 @@
data.preferredWidth!!.toInt()
}
+ val margins: PaneMargins = data.paneMargins
+
val isAnimatedPane = data.isAnimatedPane
var measuredWidth = 0
diff --git a/compose/material3/adaptive/adaptive-navigation/build.gradle b/compose/material3/adaptive/adaptive-navigation/build.gradle
index dbbcdc2..7992414 100644
--- a/compose/material3/adaptive/adaptive-navigation/build.gradle
+++ b/compose/material3/adaptive/adaptive-navigation/build.gradle
@@ -109,10 +109,6 @@
metalavaK2UastEnabled = false
}
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
-}
-
// Screenshot tests related setup
android {
compileSdk 35
diff --git a/compose/material3/adaptive/adaptive/build.gradle b/compose/material3/adaptive/adaptive/build.gradle
index 1cb594e..24f0da1 100644
--- a/compose/material3/adaptive/adaptive/build.gradle
+++ b/compose/material3/adaptive/adaptive/build.gradle
@@ -109,10 +109,6 @@
metalavaK2UastEnabled = false
}
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
-}
-
// Screenshot tests related setup
android {
compileSdk 35
diff --git a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/FloatingActionButtonBenchmark.kt b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/FloatingActionButtonBenchmark.kt
index 2585509..2e7efb0 100644
--- a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/FloatingActionButtonBenchmark.kt
+++ b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/FloatingActionButtonBenchmark.kt
@@ -18,9 +18,14 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
-import androidx.compose.material3.ExtendedFloatingActionButton
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.LargeExtendedFloatingActionButton
+import androidx.compose.material3.LargeFloatingActionButton
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.MediumExtendedFloatingActionButton
+import androidx.compose.material3.MediumFloatingActionButton
+import androidx.compose.material3.SmallExtendedFloatingActionButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.testutils.LayeredComposeTestCase
@@ -32,21 +37,21 @@
import androidx.compose.testutils.benchmark.benchmarkToFirstPixel
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
@LargeTest
-@RunWith(AndroidJUnit4::class)
-class FloatingActionButtonBenchmark {
+@RunWith(Parameterized::class)
+class FloatingActionButtonBenchmark(private val size: FabSize) {
@get:Rule val benchmarkRule = ComposeBenchmarkRule()
- private val fabTestCaseFactory = { FloatingActionButtonTestCase() }
- private val extendedFabTestCaseFactory = { ExtendedFloatingActionButtonTestCase() }
+ private val fabTestCaseFactory = { FloatingActionButtonTestCase(size) }
+ private val extendedFabTestCaseFactory = { ExtendedFloatingActionButtonTestCase(size) }
@Ignore
@Test
@@ -105,13 +110,33 @@
fun extendedFab_firstPixel() {
benchmarkRule.benchmarkToFirstPixel(extendedFabTestCaseFactory)
}
+
+ companion object {
+ @Parameterized.Parameters(name = "size = {0}")
+ @JvmStatic
+ fun parameters() = FabSize.values()
+ }
}
-internal class FloatingActionButtonTestCase : LayeredComposeTestCase() {
+internal class FloatingActionButtonTestCase(private val size: FabSize) : LayeredComposeTestCase() {
+ @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
override fun MeasuredContent() {
- FloatingActionButton(onClick = { /*TODO*/ }) { Box(modifier = Modifier.size(24.dp)) }
+ when (size) {
+ FabSize.Small ->
+ FloatingActionButton(onClick = { /*TODO*/ }) {
+ Box(modifier = Modifier.size(24.dp))
+ }
+ FabSize.Medium ->
+ MediumFloatingActionButton(onClick = { /*TODO*/ }) {
+ Box(modifier = Modifier.size(24.dp))
+ }
+ FabSize.Large ->
+ LargeFloatingActionButton(onClick = { /*TODO*/ }) {
+ Box(modifier = Modifier.size(24.dp))
+ }
+ }
}
@Composable
@@ -120,15 +145,32 @@
}
}
-internal class ExtendedFloatingActionButtonTestCase : LayeredComposeTestCase() {
+internal class ExtendedFloatingActionButtonTestCase(private val size: FabSize) :
+ LayeredComposeTestCase() {
+ @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
override fun MeasuredContent() {
- ExtendedFloatingActionButton(
- text = { Text(text = "Extended FAB") },
- icon = { Box(modifier = Modifier.size(24.dp)) },
- onClick = { /*TODO*/ }
- )
+ when (size) {
+ FabSize.Small ->
+ SmallExtendedFloatingActionButton(
+ text = { Text(text = "Extended FAB") },
+ icon = { Box(modifier = Modifier.size(24.dp)) },
+ onClick = { /*TODO*/ }
+ )
+ FabSize.Medium ->
+ MediumExtendedFloatingActionButton(
+ text = { Text(text = "Extended FAB") },
+ icon = { Box(modifier = Modifier.size(24.dp)) },
+ onClick = { /*TODO*/ }
+ )
+ FabSize.Large ->
+ LargeExtendedFloatingActionButton(
+ text = { Text(text = "Extended FAB") },
+ icon = { Box(modifier = Modifier.size(24.dp)) },
+ onClick = { /*TODO*/ }
+ )
+ }
}
@Composable
@@ -136,3 +178,9 @@
MaterialTheme { content() }
}
}
+
+enum class FabSize {
+ Small,
+ Medium,
+ Large,
+}
diff --git a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/FloatingActionButtonMenuBenchmark.kt b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/FloatingActionButtonMenuBenchmark.kt
new file mode 100644
index 0000000..db004a2
--- /dev/null
+++ b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/FloatingActionButtonMenuBenchmark.kt
@@ -0,0 +1,136 @@
+/*
+ * 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.material3.benchmark
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.FloatingActionButtonMenu
+import androidx.compose.material3.FloatingActionButtonMenuItem
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ToggleFloatingActionButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.ToggleableTestCase
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.benchmarkFirstCompose
+import androidx.compose.testutils.benchmark.benchmarkFirstDraw
+import androidx.compose.testutils.benchmark.benchmarkFirstLayout
+import androidx.compose.testutils.benchmark.benchmarkFirstMeasure
+import androidx.compose.testutils.benchmark.benchmarkToFirstPixel
+import androidx.compose.testutils.benchmark.toggleStateBenchmarkComposeMeasureLayout
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class FloatingActionButtonMenuBenchmark {
+
+ @get:Rule val benchmarkRule = ComposeBenchmarkRule()
+
+ private val floatingActionButtonMenuTestCaseFactory = { FloatingActionButtonMenuTestCase() }
+
+ @Ignore
+ @Test
+ fun fabMenu_first_compose() {
+ benchmarkRule.benchmarkFirstCompose(floatingActionButtonMenuTestCaseFactory)
+ }
+
+ @Ignore
+ @Test
+ fun fabMenu_measure() {
+ benchmarkRule.benchmarkFirstMeasure(floatingActionButtonMenuTestCaseFactory)
+ }
+
+ @Ignore
+ @Test
+ fun fabMenu_layout() {
+ benchmarkRule.benchmarkFirstLayout(floatingActionButtonMenuTestCaseFactory)
+ }
+
+ @Ignore
+ @Test
+ fun fabMenu_draw() {
+ benchmarkRule.benchmarkFirstDraw(floatingActionButtonMenuTestCaseFactory)
+ }
+
+ @Test
+ fun fabMenu_firstPixel() {
+ benchmarkRule.benchmarkToFirstPixel(floatingActionButtonMenuTestCaseFactory)
+ }
+
+ @Test
+ fun fabMenu_toggle_recomposeMeasureLayout() {
+ benchmarkRule.toggleStateBenchmarkComposeMeasureLayout(
+ caseFactory = floatingActionButtonMenuTestCaseFactory,
+ assertOneRecomposition = false
+ )
+ }
+}
+
+internal class FloatingActionButtonMenuTestCase : LayeredComposeTestCase(), ToggleableTestCase {
+
+ private var state by mutableStateOf(false)
+
+ @OptIn(ExperimentalMaterial3ExpressiveApi::class)
+ @Composable
+ override fun MeasuredContent() {
+ Box(Modifier.fillMaxSize()) {
+ FloatingActionButtonMenu(
+ modifier = Modifier.align(Alignment.BottomEnd),
+ expanded = state,
+ button = {
+ ToggleFloatingActionButton(
+ checked = state,
+ onCheckedChange = { /* Do nothing */ }
+ ) {
+ Spacer(Modifier.size(24.dp))
+ }
+ }
+ ) {
+ repeat(6) {
+ FloatingActionButtonMenuItem(
+ onClick = { /* Do nothing */ },
+ icon = { Spacer(Modifier.size(24.dp)) },
+ text = { Spacer(Modifier.size(24.dp)) },
+ )
+ }
+ }
+ }
+ }
+
+ @Composable
+ override fun ContentWrappers(content: @Composable () -> Unit) {
+ MaterialTheme { content() }
+ }
+
+ override fun toggleState() {
+ state = !state
+ }
+}
diff --git a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/IconToggleButtonBenchmark.kt b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/IconToggleButtonBenchmark.kt
index f59f5af..b7d74ba 100644
--- a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/IconToggleButtonBenchmark.kt
+++ b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/IconToggleButtonBenchmark.kt
@@ -18,9 +18,12 @@
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Lock
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledIconToggleButton
import androidx.compose.material3.FilledTonalIconToggleButton
import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButtonDefaults
+import androidx.compose.material3.IconButtonShapes
import androidx.compose.material3.IconToggleButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedIconToggleButton
@@ -92,6 +95,7 @@
private var state by mutableStateOf(false)
+ @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
override fun MeasuredContent() {
when (type) {
@@ -117,6 +121,58 @@
) {
Icon(Icons.Outlined.Lock, contentDescription = "Localized description")
}
+ IconToggleButtonType.IconToggleButtonExpressive ->
+ IconToggleButton(
+ checked = state,
+ onCheckedChange = { /* Do something! */ },
+ shapes =
+ IconButtonShapes(
+ shape = IconButtonDefaults.smallRoundShape,
+ pressedShape = IconButtonDefaults.smallPressedShape,
+ checkedShape = IconButtonDefaults.smallSquareShape
+ )
+ ) {
+ Icon(Icons.Outlined.Lock, contentDescription = "Localized description")
+ }
+ IconToggleButtonType.FilledIconToggleButtonExpressive ->
+ FilledIconToggleButton(
+ checked = state,
+ onCheckedChange = { /* Do something! */ },
+ shapes =
+ IconButtonShapes(
+ shape = IconButtonDefaults.smallRoundShape,
+ pressedShape = IconButtonDefaults.smallPressedShape,
+ checkedShape = IconButtonDefaults.smallSquareShape
+ )
+ ) {
+ Icon(Icons.Outlined.Lock, contentDescription = "Localized description")
+ }
+ IconToggleButtonType.FilledTonalIconToggleButtonExpressive ->
+ FilledTonalIconToggleButton(
+ checked = state,
+ onCheckedChange = { /* Do something! */ },
+ shapes =
+ IconButtonShapes(
+ shape = IconButtonDefaults.smallRoundShape,
+ pressedShape = IconButtonDefaults.smallPressedShape,
+ checkedShape = IconButtonDefaults.smallSquareShape
+ )
+ ) {
+ Icon(Icons.Outlined.Lock, contentDescription = "Localized description")
+ }
+ IconToggleButtonType.OutlinedIconToggleButtonExpressive ->
+ OutlinedIconToggleButton(
+ checked = state,
+ onCheckedChange = { /* Do something! */ },
+ shapes =
+ IconButtonShapes(
+ shape = IconButtonDefaults.smallRoundShape,
+ pressedShape = IconButtonDefaults.smallPressedShape,
+ checkedShape = IconButtonDefaults.smallSquareShape
+ )
+ ) {
+ Icon(Icons.Outlined.Lock, contentDescription = "Localized description")
+ }
}
}
@@ -134,5 +190,9 @@
IconToggleButton,
FilledIconToggleButton,
FilledTonalIconToggleButton,
- OutlinedIconToggleButton
+ OutlinedIconToggleButton,
+ IconToggleButtonExpressive,
+ FilledIconToggleButtonExpressive,
+ FilledTonalIconToggleButtonExpressive,
+ OutlinedIconToggleButtonExpressive
}
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index f6366cc..cc1c6bb 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -2939,9 +2939,9 @@
}
public final class WavyProgressIndicatorKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void CircularWavyProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float wavelength);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void CircularWavyProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional @FloatRange(from=0.0, to=1.0) float amplitude, optional float wavelength, optional float waveSpeed);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void CircularWavyProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> amplitude, optional float wavelength, optional float waveSpeed);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void LinearWavyProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float wavelength);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void LinearWavyProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional @FloatRange(from=0.0, to=1.0) float amplitude, optional float wavelength, optional float waveSpeed);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void LinearWavyProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float stopSize, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> amplitude, optional float wavelength, optional float waveSpeed);
}
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index f6366cc..cc1c6bb 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -2939,9 +2939,9 @@
}
public final class WavyProgressIndicatorKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void CircularWavyProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float wavelength);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void CircularWavyProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional @FloatRange(from=0.0, to=1.0) float amplitude, optional float wavelength, optional float waveSpeed);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void CircularWavyProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> amplitude, optional float wavelength, optional float waveSpeed);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void LinearWavyProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float wavelength);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void LinearWavyProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional @FloatRange(from=0.0, to=1.0) float amplitude, optional float wavelength, optional float waveSpeed);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void LinearWavyProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float stopSize, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> amplitude, optional float wavelength, optional float waveSpeed);
}
diff --git a/compose/material3/material3/build.gradle b/compose/material3/material3/build.gradle
index fd903f1..e41c773 100644
--- a/compose/material3/material3/build.gradle
+++ b/compose/material3/material3/build.gradle
@@ -107,7 +107,7 @@
implementation(project(':compose:foundation:foundation'))
implementation("androidx.compose.material:material-icons-core:1.6.8")
implementation(project(":test:screenshot:screenshot"))
- implementation(projectOrArtifact(":core:core"))
+ implementation(project(":core:core"))
implementation(libs.testRules)
implementation(libs.testRunner)
implementation(libs.junit)
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt
index 61654d2..3e166ef 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt
@@ -30,6 +30,7 @@
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.requiredSize
@@ -40,6 +41,7 @@
import androidx.compose.material3.internal.getString
import androidx.compose.material3.tokens.SheetBottomTokens
import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -84,9 +86,11 @@
import androidx.compose.ui.test.swipeUp
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.coerceAtMost
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.round
import androidx.compose.ui.unit.width
import androidx.compose.ui.zIndex
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -96,6 +100,8 @@
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import junit.framework.TestCase
+import junit.framework.TestCase.assertEquals
+import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@@ -903,4 +909,121 @@
with(density!!) { rule.rootHeight().toPx() - peekHeight.toPx() - snackbarSize!!.height }
assertThat(snackbarBottomOffset).isWithin(1f).of(expectedSnackbarBottomOffset)
}
+
+ @Test
+ fun bottomSheetScaffold_bottomSheetOffsetTaggedAsMotionFrameOfReference() {
+ var offset by mutableStateOf(IntOffset(0, 0))
+ val offsets =
+ listOf(
+ IntOffset(0, 0),
+ IntOffset(5, 20),
+ IntOffset(25, 0),
+ IntOffset(100, 10),
+ )
+ var sheetCoords: LayoutCoordinates? = null
+ var rootCoords: LayoutCoordinates? = null
+ val state = SheetState(false, density = Density(1f))
+ var sheetValue by mutableStateOf(SheetValue.Hidden)
+ rule.setContent {
+ Box(Modifier.onGloballyPositioned { rootCoords = it }.offset { offset }) {
+ LaunchedEffect(sheetValue) {
+ if (sheetValue == SheetValue.Hidden) {
+ state.hide()
+ } else if (sheetValue == SheetValue.PartiallyExpanded) {
+ state.partialExpand()
+ } else {
+ state.expand()
+ }
+ }
+ BottomSheetScaffold(
+ sheetContent = {
+ Box(Modifier.fillMaxSize().onGloballyPositioned { sheetCoords = it })
+ },
+ scaffoldState =
+ BottomSheetScaffoldState(state, remember { SnackbarHostState() })
+ ) {
+ Box(Modifier.fillMaxSize())
+ }
+ }
+ }
+
+ SheetValue.values().forEach {
+ sheetValue = it
+ rule.waitForIdle()
+
+ repeat(4) {
+ offset = offsets[it]
+ rule.runOnIdle {
+ val excludeOffset =
+ rootCoords!!
+ .localPositionOf(sheetCoords!!, includeMotionFrameOfReference = false)
+ .round()
+ val includeSheetOffset =
+ rootCoords!!
+ .localPositionOf(sheetCoords!!, includeMotionFrameOfReference = true)
+ .round()
+ assertEquals(
+ includeSheetOffset - IntOffset(0, state.requireOffset().roundToInt()),
+ excludeOffset
+ )
+ }
+ }
+ }
+ }
+
+ @Test
+ fun modalBottomSheet_bottomSheetOffsetTaggedAsMotionFrameOfReference() {
+ var offset by mutableStateOf(IntOffset(0, 0))
+ val offsets =
+ listOf(
+ IntOffset(0, 0),
+ IntOffset(5, 20),
+ IntOffset(25, 0),
+ IntOffset(100, 10),
+ )
+ var sheetCoords: LayoutCoordinates? = null
+ val state = SheetState(false, density = Density(1f))
+ var sheetValue by mutableStateOf(SheetValue.Hidden)
+ rule.setContent {
+ LaunchedEffect(sheetValue) {
+ if (sheetValue == SheetValue.Hidden) {
+ state.hide()
+ } else if (sheetValue == SheetValue.PartiallyExpanded) {
+ state.partialExpand()
+ } else {
+ state.expand()
+ }
+ }
+ ModalBottomSheet({}, sheetState = state) {
+ Box(Modifier.fillMaxSize().onGloballyPositioned { sheetCoords = it })
+ }
+ }
+
+ fun LayoutCoordinates.root(): LayoutCoordinates =
+ if (parentLayoutCoordinates != null) parentLayoutCoordinates!!.root() else this
+
+ SheetValue.values().forEach {
+ sheetValue = it
+ rule.waitForIdle()
+ val rootCoords = sheetCoords!!.root()
+
+ repeat(4) {
+ offset = offsets[it]
+ rule.runOnIdle {
+ val excludeOffset =
+ rootCoords
+ .localPositionOf(sheetCoords!!, includeMotionFrameOfReference = false)
+ .round()
+ val includeSheetOffset =
+ rootCoords
+ .localPositionOf(sheetCoords!!, includeMotionFrameOfReference = true)
+ .round()
+ assertEquals(
+ includeSheetOffset - IntOffset(0, state.requireOffset().roundToInt()),
+ excludeOffset
+ )
+ }
+ }
+ }
+ }
}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/IconButtonScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/IconButtonScreenshotTest.kt
index d57c4a2..da0d64f 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/IconButtonScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/IconButtonScreenshotTest.kt
@@ -24,6 +24,8 @@
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.filled.FavoriteBorder
+import androidx.compose.material.icons.outlined.Favorite
import androidx.compose.material.icons.outlined.FavoriteBorder
import androidx.compose.testutils.assertAgainstGolden
import androidx.compose.ui.Alignment
@@ -190,7 +192,7 @@
rule.setMaterialContent(lightColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
IconToggleButton(checked = false, onCheckedChange = { /* doSomething() */ }) {
- Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ Icon(Icons.Outlined.Favorite, contentDescription = "Localized description")
}
}
}
@@ -202,7 +204,7 @@
rule.setMaterialContent(darkColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
IconToggleButton(checked = false, onCheckedChange = { /* doSomething() */ }) {
- Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ Icon(Icons.Outlined.Favorite, contentDescription = "Localized description")
}
}
}
@@ -274,7 +276,7 @@
rule.setMaterialContent(lightColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
FilledIconToggleButton(checked = false, onCheckedChange = { /* doSomething() */ }) {
- Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ Icon(Icons.Outlined.Favorite, contentDescription = "Localized description")
}
}
}
@@ -290,7 +292,7 @@
onCheckedChange = { /* doSomething() */ },
enabled = false
) {
- Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ Icon(Icons.Outlined.Favorite, contentDescription = "Localized description")
}
}
}
@@ -302,7 +304,7 @@
rule.setMaterialContent(darkColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
FilledIconToggleButton(checked = false, onCheckedChange = { /* doSomething() */ }) {
- Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ Icon(Icons.Outlined.Favorite, contentDescription = "Localized description")
}
}
}
@@ -377,7 +379,7 @@
checked = false,
onCheckedChange = { /* doSomething() */ }
) {
- Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ Icon(Icons.Outlined.Favorite, contentDescription = "Localized description")
}
}
}
@@ -392,7 +394,7 @@
checked = false,
onCheckedChange = { /* doSomething() */ }
) {
- Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ Icon(Icons.Outlined.Favorite, contentDescription = "Localized description")
}
}
}
@@ -434,10 +436,7 @@
rule.setMaterialContent(lightColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
OutlinedIconButton(onClick = { /* doSomething() */ }) {
- Icon(
- Icons.Outlined.FavoriteBorder,
- contentDescription = "Localized description"
- )
+ Icon(Icons.Filled.FavoriteBorder, contentDescription = "Localized description")
}
}
}
@@ -449,10 +448,7 @@
rule.setMaterialContent(lightColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
OutlinedIconButton(onClick = { /* doSomething() */ }, enabled = false) {
- Icon(
- Icons.Outlined.FavoriteBorder,
- contentDescription = "Localized description"
- )
+ Icon(Icons.Filled.FavoriteBorder, contentDescription = "Localized description")
}
}
}
@@ -464,10 +460,7 @@
rule.setMaterialContent(darkColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
OutlinedIconButton(onClick = { /* doSomething() */ }) {
- Icon(
- Icons.Outlined.FavoriteBorder,
- contentDescription = "Localized description"
- )
+ Icon(Icons.Filled.FavoriteBorder, contentDescription = "Localized description")
}
}
}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonScreenshotTest.kt
index 06b13ec..bfe9fa0 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonScreenshotTest.kt
@@ -23,7 +23,8 @@
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.outlined.Edit
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.outlined.KeyboardArrowDown
import androidx.compose.testutils.assertAgainstGolden
import androidx.compose.ui.Alignment
@@ -33,6 +34,7 @@
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.test.screenshot.AndroidXScreenshotTestRule
@@ -52,6 +54,8 @@
private val wrap = Modifier.wrapContentSize(Alignment.Center)
private val wrapperTestTag = "splitButtonWrapper"
+ private val leadingButtonTag = "leadingButton"
+ private val trailingButtonTag = "trailingButton"
@Test
fun splitButton() {
@@ -63,7 +67,7 @@
onClick = { /* Do Nothing */ },
) {
Icon(
- Icons.Outlined.Edit,
+ Icons.Filled.Edit,
modifier = Modifier.size(SplitButtonDefaults.LeadingIconSize),
contentDescription = "Localized description",
)
@@ -100,7 +104,7 @@
checked = false,
leadingContent = {
Icon(
- Icons.Outlined.Edit,
+ Icons.Filled.Edit,
contentDescription = "Localized description",
Modifier.size(SplitButtonDefaults.LeadingIconSize)
)
@@ -136,7 +140,7 @@
onTrailingButtonClick = {},
leadingContent = {
Icon(
- Icons.Outlined.Edit,
+ Icons.Filled.Edit,
contentDescription = "Localized description",
Modifier.size(SplitButtonDefaults.LeadingIconSize)
)
@@ -145,7 +149,7 @@
},
trailingContent = {
Icon(
- Icons.Outlined.KeyboardArrowDown,
+ Icons.Filled.KeyboardArrowDown,
modifier =
Modifier.size(SplitButtonDefaults.TrailingIconSize).graphicsLayer {
this.rotationZ = 180f
@@ -170,7 +174,7 @@
checked = false,
leadingContent = {
Icon(
- Icons.Outlined.Edit,
+ Icons.Filled.Edit,
contentDescription = "Localized description",
Modifier.size(SplitButtonDefaults.LeadingIconSize)
)
@@ -203,7 +207,7 @@
checked = false,
leadingContent = {
Icon(
- Icons.Outlined.Edit,
+ Icons.Filled.Edit,
contentDescription = "Localized description",
Modifier.size(SplitButtonDefaults.LeadingIconSize)
)
@@ -236,7 +240,7 @@
checked = false,
leadingContent = {
Icon(
- Icons.Outlined.Edit,
+ Icons.Filled.Edit,
contentDescription = "Localized description",
Modifier.size(SplitButtonDefaults.LeadingIconSize)
)
@@ -269,7 +273,7 @@
onClick = { /* Do Nothing */ },
) {
Icon(
- Icons.Outlined.Edit,
+ Icons.Filled.Edit,
contentDescription = "Localized description",
Modifier.size(SplitButtonDefaults.LeadingIconSize)
)
@@ -322,6 +326,82 @@
assertAgainstGolden("splitButton_textLeadingButton_${scheme.name}")
}
+ @Test
+ fun splitButton_leadingButton_pressed() {
+ rule.setMaterialContent(scheme.colorScheme) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ SplitButton(
+ leadingButton = {
+ SplitButtonDefaults.LeadingButton(
+ onClick = { /* Do Nothing */ },
+ modifier = Modifier.testTag(leadingButtonTag),
+ ) {
+ Icon(
+ Icons.Filled.Edit,
+ modifier = Modifier.size(SplitButtonDefaults.LeadingIconSize),
+ contentDescription = "Localized description",
+ )
+ Spacer(Modifier.size(ButtonDefaults.IconSpacing))
+ Text("My Button")
+ }
+ },
+ trailingButton = {
+ SplitButtonDefaults.TrailingButton(
+ onClick = {},
+ checked = false,
+ ) {
+ Icon(
+ Icons.Outlined.KeyboardArrowDown,
+ contentDescription = "Localized description",
+ Modifier.size(SplitButtonDefaults.TrailingIconSize)
+ )
+ }
+ }
+ )
+ }
+ }
+
+ assertPressed(leadingButtonTag, "splitButton_leadingButton_pressed_${scheme.name}")
+ }
+
+ @Test
+ fun splitButton_trailingButton_pressed() {
+ rule.setMaterialContent(scheme.colorScheme) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ SplitButton(
+ leadingButton = {
+ SplitButtonDefaults.LeadingButton(
+ onClick = { /* Do Nothing */ },
+ ) {
+ Icon(
+ Icons.Filled.Edit,
+ modifier = Modifier.size(SplitButtonDefaults.LeadingIconSize),
+ contentDescription = "Localized description",
+ )
+ Spacer(Modifier.size(ButtonDefaults.IconSpacing))
+ Text("My Button")
+ }
+ },
+ trailingButton = {
+ SplitButtonDefaults.TrailingButton(
+ onClick = {},
+ checked = false,
+ modifier = Modifier.testTag(trailingButtonTag),
+ ) {
+ Icon(
+ Icons.Outlined.KeyboardArrowDown,
+ contentDescription = "Localized description",
+ Modifier.size(SplitButtonDefaults.TrailingIconSize)
+ )
+ }
+ }
+ )
+ }
+ }
+
+ assertPressed(trailingButtonTag, "splitButton_trailingButton_pressed_${scheme.name}")
+ }
+
private fun assertAgainstGolden(goldenName: String) {
rule
.onNodeWithTag(wrapperTestTag)
@@ -329,6 +409,21 @@
.assertAgainstGolden(screenshotRule, goldenName)
}
+ private fun assertPressed(tag: String, goldenName: String) {
+ rule.mainClock.autoAdvance = false
+ rule.onNodeWithTag(tag).performTouchInput { down(center) }
+
+ rule.mainClock.advanceTimeByFrame()
+ rule.waitForIdle() // Wait for measure
+ rule.mainClock.advanceTimeBy(milliseconds = 200)
+
+ // Ripples are drawn on the RenderThread, not the main (UI) thread, so we can't wait for
+ // synchronization. Instead just wait until after the ripples are finished animating.
+ Thread.sleep(300)
+
+ assertAgainstGolden(goldenName)
+ }
+
// Provide the ColorScheme and their name parameter in a ColorSchemeWrapper.
// This makes sure that the default method name and the initial Scuba image generated
// name is as expected.
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ToggleButtonScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ToggleButtonScreenshotTest.kt
index d6dcd91..a699d92 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ToggleButtonScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ToggleButtonScreenshotTest.kt
@@ -22,6 +22,7 @@
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.outlined.Favorite
import androidx.compose.testutils.assertAgainstGolden
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
@@ -448,7 +449,7 @@
Box(Modifier.testTag(wrapperTestTag)) {
ToggleButton(checked = false, onCheckedChange = {}) {
Icon(
- Icons.Filled.Favorite,
+ Icons.Outlined.Favorite,
contentDescription = "Localized description",
modifier = Modifier.size(ToggleButtonDefaults.IconSize)
)
@@ -466,7 +467,7 @@
Box(Modifier.testTag(wrapperTestTag)) {
ToggleButton(checked = false, onCheckedChange = {}, enabled = false) {
Icon(
- Icons.Filled.Favorite,
+ Icons.Outlined.Favorite,
contentDescription = "Localized description",
modifier = Modifier.size(ToggleButtonDefaults.IconSize)
)
@@ -484,7 +485,7 @@
Box(Modifier.testTag(wrapperTestTag)) {
ToggleButton(checked = false, onCheckedChange = {}) {
Icon(
- Icons.Filled.Favorite,
+ Icons.Outlined.Favorite,
contentDescription = "Localized description",
modifier = Modifier.size(ToggleButtonDefaults.IconSize)
)
@@ -539,7 +540,7 @@
Box(Modifier.testTag(wrapperTestTag)) {
ToggleButton(checked = false, onCheckedChange = {}) {
Icon(
- Icons.Filled.Favorite,
+ Icons.Outlined.Favorite,
contentDescription = "Localized description",
modifier = Modifier.size(ToggleButtonDefaults.IconSize)
)
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WavyProgressIndicatorScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WavyProgressIndicatorScreenshotTest.kt
index 5852853..90f46c5 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WavyProgressIndicatorScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WavyProgressIndicatorScreenshotTest.kt
@@ -79,6 +79,18 @@
}
@Test
+ fun linearWavyProgressIndicator_indeterminate_lowerAmplitude() {
+ rule.mainClock.autoAdvance = false
+ rule.setMaterialContent(scheme.colorScheme) {
+ Box(wrap.testTag(wrapperTestTag)) { LinearWavyProgressIndicator(amplitude = 0.5f) }
+ }
+ rule.mainClock.advanceTimeBy(1200)
+ assertIndicatorAgainstGolden(
+ "linearWavyProgressIndicator_indeterminate_lowerAmplitude_${scheme.name}"
+ )
+ }
+
+ @Test
fun linearWavyProgressIndicator_midProgress_determinate() {
rule.setMaterialContent(scheme.colorScheme) {
Box(wrap.testTag(wrapperTestTag)) { LinearWavyProgressIndicator(progress = { 0.5f }) }
@@ -253,6 +265,18 @@
}
@Test
+ fun circularWavyProgressIndicator_indeterminate_lowerAmplitude() {
+ rule.mainClock.autoAdvance = false
+ rule.setMaterialContent(scheme.colorScheme) {
+ Box(wrap.testTag(wrapperTestTag)) { CircularWavyProgressIndicator(amplitude = 0.5f) }
+ }
+ rule.mainClock.advanceTimeBy(500)
+ assertIndicatorAgainstGolden(
+ "circularWavyProgressIndicator_indeterminate_lowerAmplitude_${scheme.name}"
+ )
+ }
+
+ @Test
fun circularWavyProgressIndicator_determinate_customCapAndTrack() {
rule.setMaterialContent(scheme.colorScheme) {
Box(wrap.testTag(wrapperTestTag)) {
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WavyProgressIndicatorTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WavyProgressIndicatorTest.kt
index 73586b6..b4fa43b 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WavyProgressIndicatorTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WavyProgressIndicatorTest.kt
@@ -208,7 +208,7 @@
}
}
- rule.mainClock.advanceTimeBy(200)
+ rule.mainClock.advanceTimeBy(300)
rule.onNodeWithTag(tag).captureToImage().toPixelMap().let {
assertEquals(expectedSize.width, it.width)
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/anchoredDraggable/AnchoredDraggableStateTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/anchoredDraggable/AnchoredDraggableStateTest.kt
index 2ca56a9..72c2356 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/anchoredDraggable/AnchoredDraggableStateTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/anchoredDraggable/AnchoredDraggableStateTest.kt
@@ -19,6 +19,7 @@
import androidx.compose.animation.core.FloatSpringSpec
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
@@ -39,16 +40,22 @@
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MonotonicFrameClock
import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.runtime.withFrameNanos
import androidx.compose.testutils.WithTouchSlop
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.layout.layout
+import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.junit4.StateRestorationTester
@@ -59,11 +66,13 @@
import androidx.compose.ui.test.swipeUp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.round
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import java.util.concurrent.TimeUnit
+import junit.framework.TestCase.assertEquals
import kotlin.math.roundToInt
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
@@ -697,7 +706,7 @@
initialValue = A,
defaultPositionalThreshold,
defaultVelocityThreshold,
- animationSpec = defaultAnimationSpec,
+ animationSpec = defaultAnimationSpec
)
anchoredDraggableState.updateAnchors(
DraggableAnchors {
@@ -735,40 +744,6 @@
dragJob.cancel()
}
- @Test
- fun anchoredDraggable_anchoredDrag_doesNotUpdateOnConfirmValueChange() = runTest {
- val anchoredDraggableState =
- AnchoredDraggableState(
- initialValue = B,
- defaultPositionalThreshold,
- defaultVelocityThreshold,
- animationSpec = defaultAnimationSpec,
- confirmValueChange = { false }
- )
- anchoredDraggableState.updateAnchors(
- DraggableAnchors {
- A at 0f
- B at 200f
- }
- )
-
- assertThat(anchoredDraggableState.targetValue).isEqualTo(B)
-
- val unexpectedTarget = A
- val targetUpdates = Channel<Float>()
- val dragJob =
- launch(Dispatchers.Unconfined) {
- anchoredDraggableState.anchoredDrag(unexpectedTarget) { anchors, latestTarget ->
- targetUpdates.send(anchors.positionOf(latestTarget))
- suspendIndefinitely()
- }
- }
-
- val firstTarget = targetUpdates.receive()
- assertThat(firstTarget).isEqualTo(200f)
- dragJob.cancel()
- }
-
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun anchoredDraggable_dragCompletesExceptionally_cleansUp() = runTest {
@@ -1019,6 +994,64 @@
)
}
+ @Test
+ fun draggableAnchors_draggableOffsetTaggedAsMotionFrameOfReference() {
+ var offset by mutableStateOf(IntOffset(0, 0))
+ val offsets =
+ listOf(
+ IntOffset(0, 0),
+ IntOffset(5, 20),
+ IntOffset(25, 0),
+ IntOffset(100, 10),
+ )
+ var coords: LayoutCoordinates? = null
+ var rootCoords: LayoutCoordinates? = null
+ val state =
+ AnchoredDraggableState(
+ initialValue = 0,
+ positionalThreshold = defaultPositionalThreshold,
+ velocityThreshold = defaultVelocityThreshold,
+ animationSpec = { spring() }
+ )
+ var value by mutableIntStateOf(0)
+ rule.setContent {
+ Box(Modifier.onGloballyPositioned { rootCoords = it }.offset { offset }) {
+ LaunchedEffect(value) { state.snapTo(value) }
+ Box(
+ Modifier.draggableAnchors(state, Orientation.Vertical) { _, _ ->
+ DraggableAnchors { repeat(5) { it at it * 100f } } to 0
+ }
+ .fillMaxSize()
+ ) {
+ Box(Modifier.fillMaxSize().onGloballyPositioned { coords = it })
+ }
+ }
+ }
+
+ repeat(5) {
+ value = it
+ rule.waitForIdle()
+
+ repeat(4) {
+ offset = offsets[it]
+ rule.runOnIdle {
+ val excludeOffset =
+ rootCoords!!
+ .localPositionOf(coords!!, includeMotionFrameOfReference = false)
+ .round()
+ val includeOffset =
+ rootCoords!!
+ .localPositionOf(coords!!, includeMotionFrameOfReference = true)
+ .round()
+ assertEquals(
+ includeOffset - IntOffset(0, state.requireOffset().roundToInt()),
+ excludeOffset
+ )
+ }
+ }
+ }
+ }
+
private suspend fun suspendIndefinitely() = suspendCancellableCoroutine<Unit> {}
private class HandPumpTestFrameClock : MonotonicFrameClock {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MaterialShapes.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MaterialShapes.kt
index b0ac90c..24ab941 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MaterialShapes.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MaterialShapes.kt
@@ -29,16 +29,11 @@
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.util.fastFlatMap
-import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastMap
-import androidx.compose.ui.util.fastMaxBy
import androidx.graphics.shapes.CornerRounding
import androidx.graphics.shapes.Morph
import androidx.graphics.shapes.RoundedPolygon
-import androidx.graphics.shapes.TransformResult
import androidx.graphics.shapes.circle
-import androidx.graphics.shapes.pill
import androidx.graphics.shapes.rectangle
import androidx.graphics.shapes.star
import kotlin.math.PI
@@ -126,20 +121,15 @@
companion object {
// Cache various roundings for use below
- private val cornerRound10 = CornerRounding(radius = .1f)
private val cornerRound15 = CornerRounding(radius = .15f)
private val cornerRound20 = CornerRounding(radius = .2f)
private val cornerRound30 = CornerRounding(radius = .3f)
- private val cornerRound40 = CornerRounding(radius = .4f)
private val cornerRound50 = CornerRounding(radius = .5f)
private val cornerRound100 = CornerRounding(radius = 1f)
private val rotateNeg45 = Matrix().apply { rotateZ(-45f) }
- private val rotate45 = Matrix().apply { rotateZ(45f) }
private val rotateNeg90 = Matrix().apply { rotateZ(-90f) }
- private val rotate90 = Matrix().apply { rotateZ(90f) }
private val rotateNeg135 = Matrix().apply { rotateZ(-135f) }
- private val unrounded = CornerRounding.Unrounded
private var _circle: RoundedPolygon? = null
private var _square: RoundedPolygon? = null
@@ -326,14 +316,13 @@
}
internal fun slanted(): RoundedPolygon {
- return RoundedPolygon(
- numVertices = 4,
- rounding = CornerRounding(radius = 0.3f, smoothing = 0.5f)
- )
- .transformed(rotateNeg45)
- .transformed { x, y ->
- TransformResult(x - 0.1f * y, y) // Compose's matrix doesn't support skew!?
- }
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.926f, 0.970f), CornerRounding(0.189f, 0.811f)),
+ PointNRound(Offset(-0.021f, 0.967f), CornerRounding(0.187f, 0.057f))
+ ),
+ 2
+ )
}
internal fun arch(): RoundedPolygon {
@@ -346,35 +335,27 @@
}
internal fun fan(): RoundedPolygon {
- return RoundedPolygon(
- numVertices = 4,
- perVertexRounding =
- listOf(cornerRound100, cornerRound20, cornerRound20, cornerRound20)
- )
- .transformed(rotateNeg45)
- }
-
- internal fun arrow(): RoundedPolygon {
- return triangleChip(
- innerRadius = .3375f,
- rounding = CornerRounding(radius = .25f, smoothing = .48f)
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(1.004f, 1.000f), CornerRounding(0.148f, 0.417f)),
+ PointNRound(Offset(0.000f, 1.000f), CornerRounding(0.151f)),
+ PointNRound(Offset(0.000f, -0.003f), CornerRounding(0.148f)),
+ PointNRound(Offset(0.978f, 0.020f), CornerRounding(0.803f))
+ ),
+ 1
)
}
- internal fun triangleChip(innerRadius: Float, rounding: CornerRounding): RoundedPolygon {
- val topR = 0.888f
- val points =
- floatArrayOf(
- radialToCartesian(radius = topR, 270f.toRadians()).x,
- radialToCartesian(radius = topR, 270f.toRadians()).y,
- radialToCartesian(radius = 1f, 30f.toRadians()).x,
- radialToCartesian(radius = 1f, 30f.toRadians()).y,
- radialToCartesian(radius = innerRadius, 90f.toRadians()).x,
- radialToCartesian(radius = innerRadius, 90f.toRadians()).y,
- radialToCartesian(radius = 1f, 150f.toRadians()).x,
- radialToCartesian(radius = 1f, 150f.toRadians()).y
- )
- return RoundedPolygon(points, rounding)
+ internal fun arrow(): RoundedPolygon {
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.500f, 0.892f), CornerRounding(0.313f)),
+ PointNRound(Offset(-0.216f, 1.050f), CornerRounding(0.207f)),
+ PointNRound(Offset(0.499f, -0.160f), CornerRounding(0.215f, 1.000f)),
+ PointNRound(Offset(1.225f, 1.060f), CornerRounding(0.211f))
+ ),
+ 1
+ )
}
internal fun semiCircle(): RoundedPolygon {
@@ -386,13 +367,21 @@
)
}
- internal fun oval(scaleX: Float = 1f, scaleY: Float = .7f): RoundedPolygon {
- val m = Matrix().apply { scale(x = scaleX, y = scaleY) }
+ internal fun oval(): RoundedPolygon {
+ val m = Matrix().apply { scale(1f, 0.64f) }
return RoundedPolygon.circle().transformed(m).transformed(rotateNeg45)
}
- internal fun pill(width: Float = 1.25f, height: Float = 1f): RoundedPolygon {
- return RoundedPolygon.pill(width = width, height = height).transformed(rotateNeg45)
+ internal fun pill(): RoundedPolygon {
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.961f, 0.039f), CornerRounding(0.426f)),
+ PointNRound(Offset(1.001f, 0.428f)),
+ PointNRound(Offset(1.000f, 0.609f), CornerRounding(1.000f))
+ ),
+ reps = 2,
+ mirroring = true
+ )
}
internal fun triangle(): RoundedPolygon {
@@ -400,77 +389,50 @@
.transformed(rotateNeg90)
}
- internal fun diamond(scaleX: Float = 1f, scaleY: Float = 1.2f): RoundedPolygon {
- return RoundedPolygon(numVertices = 4, rounding = cornerRound30)
- .transformed(Matrix().apply { scale(x = scaleX, y = scaleY) })
+ internal fun diamond(): RoundedPolygon {
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.500f, 1.096f), CornerRounding(0.151f, 0.524f)),
+ PointNRound(Offset(0.040f, 0.500f), CornerRounding(0.159f))
+ ),
+ 2
+ )
}
internal fun clamShell(): RoundedPolygon {
- val cornerInset = .6f
- val edgeInset = .4f
- val height = .7f
- val hexPoints =
- floatArrayOf(
- 1f,
- 0f,
- cornerInset,
- height,
- edgeInset,
- height,
- -edgeInset,
- height,
- -cornerInset,
- height,
- -1f,
- 0f,
- -cornerInset,
- -height,
- -edgeInset,
- -height,
- edgeInset,
- -height,
- cornerInset,
- -height,
- )
- val pvRounding =
+ return customPolygon(
listOf(
- cornerRound30,
- cornerRound30,
- unrounded,
- unrounded,
- cornerRound30,
- cornerRound30,
- cornerRound30,
- unrounded,
- unrounded,
- cornerRound30,
- )
- return RoundedPolygon(hexPoints, perVertexRounding = pvRounding)
+ PointNRound(Offset(0.171f, 0.841f), CornerRounding(0.159f)),
+ PointNRound(Offset(-0.020f, 0.500f), CornerRounding(0.140f)),
+ PointNRound(Offset(0.170f, 0.159f), CornerRounding(0.159f))
+ ),
+ 2
+ )
}
internal fun pentagon(): RoundedPolygon {
- return RoundedPolygon(numVertices = 5, rounding = cornerRound30)
- .transformed(Matrix().apply { rotateZ(-360f / 20f) })
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.500f, -0.009f), CornerRounding(0.172f)),
+ PointNRound(Offset(1.030f, 0.365f), CornerRounding(0.164f)),
+ PointNRound(Offset(0.828f, 0.970f), CornerRounding(0.169f))
+ ),
+ reps = 1,
+ mirroring = true
+ )
}
internal fun gem(): RoundedPolygon {
- // irregular hexagon (right narrower than left, then rotated)
- // First, generate a standard hexagon
- val numVertices = 6
- val radius = 1f
- val points = FloatArray(numVertices * 2)
- var index = 0
- for (i in 0 until numVertices) {
- val vertex = radialToCartesian(radius, (PI.toFloat() / numVertices * 2 * i))
- points[index++] = vertex.x
- points[index++] = vertex.y
- }
- // Now adjust-in the points at the top (next-to-last and second vertices, post rotation)
- points[2] -= .1f
- points[3] -= .1f
- points[10] -= .1f
- points[11] += .1f
- return RoundedPolygon(points, cornerRound40).transformed(rotateNeg90)
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.499f, 1.023f), CornerRounding(0.241f, 0.778f)),
+ PointNRound(Offset(-0.005f, 0.792f), CornerRounding(0.208f)),
+ PointNRound(Offset(0.073f, 0.258f), CornerRounding(0.228f)),
+ PointNRound(Offset(0.433f, -0.000f), CornerRounding(0.491f))
+ ),
+ 1,
+ mirroring = true
+ )
}
internal fun sunny(): RoundedPolygon {
@@ -482,30 +444,34 @@
}
internal fun verySunny(): RoundedPolygon {
- return RoundedPolygon.star(
- numVerticesPerRadius = 8,
- innerRadius = .65f,
- rounding = cornerRound15
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.500f, 1.080f), CornerRounding(0.085f)),
+ PointNRound(Offset(0.358f, 0.843f), CornerRounding(0.085f))
+ ),
+ 8
)
}
internal fun cookie4(): RoundedPolygon {
- return RoundedPolygon.star(
- numVerticesPerRadius = 4,
- innerRadius = .5f,
- rounding = cornerRound30
- )
- .transformed(rotateNeg45)
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(1.237f, 1.236f), CornerRounding(0.258f)),
+ PointNRound(Offset(0.500f, 0.918f), CornerRounding(0.233f))
+ ),
+ 4
+ )
}
internal fun cookie6(): RoundedPolygon {
// 6-point cookie
- return RoundedPolygon.star(
- numVerticesPerRadius = 6,
- innerRadius = .75f,
- rounding = cornerRound50
- )
- .transformed(rotateNeg90)
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.723f, 0.884f), CornerRounding(0.394f)),
+ PointNRound(Offset(0.500f, 1.099f), CornerRounding(0.398f))
+ ),
+ 6
+ )
}
internal fun cookie7(): RoundedPolygon {
@@ -537,303 +503,199 @@
}
internal fun ghostish(): RoundedPolygon {
- val inset = .46f
- val h = 1.2f
- val points = floatArrayOf(-1f, -h, 1f, -h, 1f, h, 0f, inset, -1f, h)
- val pvRounding =
- listOf(cornerRound100, cornerRound100, cornerRound50, cornerRound100, cornerRound50)
- return RoundedPolygon(points, perVertexRounding = pvRounding)
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.500f, 0f), CornerRounding(1.000f)),
+ PointNRound(Offset(1f, 0f), CornerRounding(1.000f)),
+ PointNRound(Offset(1f, 1.140f), CornerRounding(0.254f, 0.106f)),
+ PointNRound(Offset(0.575f, 0.906f), CornerRounding(0.253f))
+ ),
+ reps = 1,
+ mirroring = true
+ )
}
internal fun clover4(): RoundedPolygon {
- // (no inner rounding)
- return RoundedPolygon.star(
- numVerticesPerRadius = 4,
- innerRadius = .2f,
- rounding = cornerRound40,
- innerRounding = unrounded
- )
- .transformed(rotate45)
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.500f, 0.074f)),
+ PointNRound(Offset(0.725f, -0.099f), CornerRounding(0.476f))
+ ),
+ reps = 4,
+ mirroring = true
+ )
}
internal fun clover8(): RoundedPolygon {
- // (no inner rounding)
- return RoundedPolygon.star(
- numVerticesPerRadius = 8,
- innerRadius = .65f,
- rounding = cornerRound30,
- innerRounding = unrounded
- )
- .transformed(Matrix().apply { rotateZ(360f / 16) })
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.500f, 0.036f)),
+ PointNRound(Offset(0.758f, -0.101f), CornerRounding(0.209f))
+ ),
+ reps = 8
+ )
}
internal fun burst(): RoundedPolygon {
- return RoundedPolygon.star(numVerticesPerRadius = 12, innerRadius = .7f)
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.500f, -0.006f), CornerRounding(0.006f)),
+ PointNRound(Offset(0.592f, 0.158f), CornerRounding(0.006f))
+ ),
+ reps = 12
+ )
}
internal fun softBurst(): RoundedPolygon {
- return RoundedPolygon.star(
- radius = 1f,
- numVerticesPerRadius = 10,
- innerRadius = .65f,
- rounding = cornerRound10,
- innerRounding = cornerRound10
- )
- .transformed(Matrix().apply { rotateZ(360f / 20) })
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.193f, 0.277f), CornerRounding(0.053f)),
+ PointNRound(Offset(0.176f, 0.055f), CornerRounding(0.053f))
+ ),
+ reps = 10
+ )
}
internal fun boom(): RoundedPolygon {
- return RoundedPolygon.star(numVerticesPerRadius = 15, innerRadius = .42f)
- .transformed(Matrix().apply { rotateZ(360f / 60) })
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.457f, 0.296f), CornerRounding(0.007f)),
+ PointNRound(Offset(0.500f, -0.051f), CornerRounding(0.007f))
+ ),
+ reps = 15
+ )
}
internal fun softBoom(): RoundedPolygon {
- val points =
- arrayOf(
- Offset(0.456f, 0.224f),
- Offset(0.460f, 0.170f),
- Offset(0.500f, 0.100f),
- Offset(0.540f, 0.170f),
- Offset(0.544f, 0.224f),
- Offset(0.538f, 0.308f)
- )
- val actualPoints = doRepeat(points, 16, center = Offset(0.5f, 0.5f))
- val roundings =
+ return customPolygon(
listOf(
- CornerRounding(radius = 0.020f),
- CornerRounding(radius = 0.143f),
- CornerRounding(radius = 0.025f),
- CornerRounding(radius = 0.143f),
- CornerRounding(radius = 0.190f),
- CornerRounding(radius = 0f)
- )
- .let { l -> (0 until 16).flatMap { l } }
-
- return RoundedPolygon(
- actualPoints,
- perVertexRounding = roundings,
- centerX = 0.5f,
- centerY = 0.5f
+ PointNRound(Offset(0.733f, 0.454f)),
+ PointNRound(Offset(0.839f, 0.437f), CornerRounding(0.532f)),
+ PointNRound(Offset(0.949f, 0.449f), CornerRounding(0.439f, 1.000f)),
+ PointNRound(Offset(0.998f, 0.478f), CornerRounding(0.174f))
+ ),
+ reps = 16,
+ mirroring = true
)
}
internal fun flower(): RoundedPolygon {
- val smoothRound = CornerRounding(radius = .12f, smoothing = .48f)
- return RoundedPolygon.star(
- numVerticesPerRadius = 8,
- radius = 1f,
- innerRadius = .588f,
- rounding = smoothRound,
- innerRounding = unrounded
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.370f, 0.187f)),
+ PointNRound(Offset(0.416f, 0.049f), CornerRounding(0.381f)),
+ PointNRound(Offset(0.479f, 0.001f), CornerRounding(0.095f))
+ ),
+ reps = 8,
+ mirroring = true
)
}
internal fun puffy(): RoundedPolygon {
- val pnr =
- listOf(
- PointNRound(Offset(0.500f, 0.260f), CornerRounding.Unrounded),
- PointNRound(Offset(0.526f, 0.188f), CornerRounding(0.095f)),
- PointNRound(Offset(0.676f, 0.226f), CornerRounding(0.095f)),
- PointNRound(Offset(0.660f, 0.300f), CornerRounding.Unrounded),
- PointNRound(Offset(0.734f, 0.230f), CornerRounding(0.095f)),
- PointNRound(Offset(0.838f, 0.350f), CornerRounding(0.095f)),
- PointNRound(Offset(0.782f, 0.418f), CornerRounding.Unrounded),
- PointNRound(Offset(0.874f, 0.414f), CornerRounding(0.095f)),
+ val m = Matrix().apply { scale(1f, 0.742f) }
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.500f, 0.053f)),
+ PointNRound(Offset(0.545f, -0.040f), CornerRounding(0.405f)),
+ PointNRound(Offset(0.670f, -0.035f), CornerRounding(0.426f)),
+ PointNRound(Offset(0.717f, 0.066f), CornerRounding(0.574f)),
+ PointNRound(Offset(0.722f, 0.128f)),
+ PointNRound(Offset(0.777f, 0.002f), CornerRounding(0.360f)),
+ PointNRound(Offset(0.914f, 0.149f), CornerRounding(0.660f)),
+ PointNRound(Offset(0.926f, 0.289f), CornerRounding(0.660f)),
+ PointNRound(Offset(0.881f, 0.346f)),
+ PointNRound(Offset(0.940f, 0.344f), CornerRounding(0.126f)),
+ PointNRound(Offset(1.003f, 0.437f), CornerRounding(0.255f)),
+ ),
+ reps = 2,
+ mirroring = true
)
- val actualPoints =
- doRepeat(pnr, reps = 4, center = Offset(0.5f, 0.5f), mirroring = true)
-
- return RoundedPolygon(
- actualPoints.fastFlatMap { listOf(it.o.x, it.o.y) }.toFloatArray(),
- perVertexRounding = actualPoints.fastMap { it.r },
- centerX = 0.5f,
- centerY = 0.5f
- )
+ .transformed(m)
}
internal fun puffyDiamond(): RoundedPolygon {
- val points =
- arrayOf(
- Offset(0.390f, 0.260f),
- Offset(0.390f, 0.130f),
- Offset(0.610f, 0.130f),
- Offset(0.610f, 0.260f),
- Offset(0.740f, 0.260f)
- )
- val actualPoints = doRepeat(points, reps = 4, center = Offset(0.5f, 0.5f))
- val roundings =
+ return customPolygon(
listOf(
- CornerRounding(radius = 0.000f),
- CornerRounding(radius = 0.104f),
- CornerRounding(radius = 0.104f),
- CornerRounding(radius = 0.000f),
- CornerRounding(radius = 0.104f)
- )
- .let { l -> (0 until 4).flatMap { l } }
-
- return RoundedPolygon(
- actualPoints,
- perVertexRounding = roundings,
- centerX = 0.5f,
- centerY = 0.5f
+ PointNRound(Offset(0.870f, 0.130f), CornerRounding(0.146f)),
+ PointNRound(Offset(0.818f, 0.357f)),
+ PointNRound(Offset(1.000f, 0.332f), CornerRounding(0.853f))
+ ),
+ reps = 4,
+ mirroring = true
)
}
@Suppress("ListIterator", "PrimitiveInCollection")
internal fun pixelCircle(): RoundedPolygon {
- val main = 0.4f
- val holes = listOf(Offset(0.28f, 0.14f), Offset(0.16f, 0.16f), Offset(0.16f, 0.3f))
- var p = Offset(main, -1f)
- val corner = buildList {
- add(p)
- holes.fastForEach { delta ->
- p += Offset(0f, delta.y)
- add(p)
- p += Offset(delta.x, 0f)
- add(p)
- }
- }
- val half = corner + corner.fastMap { Offset(it.x, -it.y) }.reversed()
- val points = half + half.fastMap { Offset(-it.x, it.y) }.reversed()
- return RoundedPolygon(
- points.fastFlatMap { listOf(it.x, it.y) }.toFloatArray(),
- unrounded
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.500f, 0.000f)),
+ PointNRound(Offset(0.704f, 0.000f)),
+ PointNRound(Offset(0.704f, 0.065f)),
+ PointNRound(Offset(0.843f, 0.065f)),
+ PointNRound(Offset(0.843f, 0.148f)),
+ PointNRound(Offset(0.926f, 0.148f)),
+ PointNRound(Offset(0.926f, 0.296f)),
+ PointNRound(Offset(1.000f, 0.296f))
+ ),
+ reps = 2,
+ mirroring = true
)
}
@Suppress("ListIterator", "PrimitiveInCollection")
internal fun pixelTriangle(): RoundedPolygon {
- var point = Offset(0f, 0f)
- val points = mutableListOf<Offset>()
- points.add(point)
- val sizes = listOf(56f, 28f, 44f, 26f, 44f, 32f, 38f, 26f, 38f, 32f)
- sizes.chunked(2).forEach { (dx, dy) ->
- point += Offset(dx, 0f)
- points.add(point)
- point += Offset(0f, dy)
- points.add(point)
- }
- point += Offset(32f, 0f)
- points.add(point)
- point += Offset(0f, 38f)
- points.add(point)
- point += Offset(-32f, 0f)
- points.add(point)
- sizes.reversed().chunked(2).forEach { (dy, dx) ->
- point += Offset(0f, dy)
- points.add(point)
- point += Offset(-dx, 0f)
- points.add(point)
- }
- val centerX = points.fastMaxBy { it.x }!!.x / 2
- val centerY = points.fastMaxBy { it.y }!!.y / 2
-
- return RoundedPolygon(
- points.fastFlatMap { listOf(it.x, it.y) }.toFloatArray(),
- centerX = centerX,
- centerY = centerY,
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.110f, 0.500f)),
+ PointNRound(Offset(0.113f, 0.000f)),
+ PointNRound(Offset(0.287f, 0.000f)),
+ PointNRound(Offset(0.287f, 0.087f)),
+ PointNRound(Offset(0.421f, 0.087f)),
+ PointNRound(Offset(0.421f, 0.170f)),
+ PointNRound(Offset(0.560f, 0.170f)),
+ PointNRound(Offset(0.560f, 0.265f)),
+ PointNRound(Offset(0.674f, 0.265f)),
+ PointNRound(Offset(0.675f, 0.344f)),
+ PointNRound(Offset(0.789f, 0.344f)),
+ PointNRound(Offset(0.789f, 0.439f)),
+ PointNRound(Offset(0.888f, 0.439f))
+ ),
+ reps = 1,
+ mirroring = true
)
}
internal fun bun(): RoundedPolygon {
- // Basically, two pills stacked on each other
- val inset = .4f
- val sandwichPoints =
- floatArrayOf(
- 1f,
- 1f,
- inset,
- 1f,
- -inset,
- 1f,
- -1f,
- 1f,
- -1f,
- 0f,
- -inset,
- 0f,
- -1f,
- 0f,
- -1f,
- -1f,
- -inset,
- -1f,
- inset,
- -1f,
- 1f,
- -1f,
- 1f,
- 0f,
- inset,
- 0f,
- 1f,
- 0f
- )
- val pvRounding =
+ return customPolygon(
listOf(
- cornerRound100,
- unrounded,
- unrounded,
- cornerRound100,
- cornerRound100,
- unrounded,
- cornerRound100,
- cornerRound100,
- unrounded,
- unrounded,
- cornerRound100,
- cornerRound100,
- unrounded,
- cornerRound100
- )
- return RoundedPolygon(sandwichPoints, perVertexRounding = pvRounding)
+ PointNRound(Offset(0.796f, 0.500f)),
+ PointNRound(Offset(0.853f, 0.518f), CornerRounding(1f)),
+ PointNRound(Offset(0.992f, 0.631f), CornerRounding(1f)),
+ PointNRound(Offset(0.968f, 1.000f), CornerRounding(1f))
+ ),
+ reps = 2,
+ mirroring = true
+ )
}
internal fun heart(): RoundedPolygon {
- val points =
- floatArrayOf(
- .2f,
- 0f,
- -.4f,
- .5f,
- -1f,
- 1f,
- -1.5f,
- .5f,
- -1f,
- 0f,
- -1.5f,
- -.5f,
- -1f,
- -1f,
- -.4f,
- -.5f
- )
- val pvRounding =
+ return customPolygon(
listOf(
- unrounded,
- unrounded,
- cornerRound100,
- cornerRound100,
- unrounded,
- cornerRound100,
- cornerRound100,
- unrounded
- )
- return RoundedPolygon(points, perVertexRounding = pvRounding).transformed(rotate90)
+ PointNRound(Offset(0.500f, 0.268f), CornerRounding(0.016f)),
+ PointNRound(Offset(0.792f, -0.066f), CornerRounding(0.958f)),
+ PointNRound(Offset(1.064f, 0.276f), CornerRounding(1.000f)),
+ PointNRound(Offset(0.501f, 0.946f), CornerRounding(0.129f))
+ ),
+ reps = 1,
+ mirroring = true
+ )
}
- private data class PointNRound(val o: Offset, val r: CornerRounding)
-
- private fun doRepeat(points: Array<Offset>, reps: Int, center: Offset) =
- points.size.let { np ->
- (0 until np * reps)
- .flatMap {
- val point = points[it % np].rotateDegrees((it / np) * 360f / reps, center)
- listOf(point.x, point.y)
- }
- .toFloatArray()
- }
+ private data class PointNRound(
+ val o: Offset,
+ val r: CornerRounding = CornerRounding.Unrounded
+ )
@Suppress("PrimitiveInCollection")
private fun doRepeat(
@@ -846,8 +708,9 @@
buildList {
val angles = points.fastMap { (it.o - center).angleDegrees() }
val distances = points.fastMap { (it.o - center).getDistance() }
- val sectionAngle = 360f / reps
- repeat(reps) {
+ val actualReps = reps * 2
+ val sectionAngle = 360f / actualReps
+ repeat(actualReps) {
points.indices.forEach { index ->
val i = if (it % 2 == 0) index else points.lastIndex - index
if (i > 0 || it % 2 == 0) {
@@ -883,13 +746,22 @@
private fun Offset.angleDegrees() = atan2(y, x) * 180f / PI.toFloat()
- private fun directionVector(angleRadians: Float) =
- Offset(cos(angleRadians), sin(angleRadians))
-
- private fun radialToCartesian(
- radius: Float,
- angleRadians: Float,
- center: Offset = Offset.Zero
- ) = directionVector(angleRadians) * radius + center
+ private fun customPolygon(
+ pnr: List<PointNRound>,
+ reps: Int,
+ center: Offset = Offset(0.5f, 0.5f),
+ mirroring: Boolean = false
+ ): RoundedPolygon {
+ val actualPoints = doRepeat(pnr, reps, center, mirroring)
+ return RoundedPolygon(
+ vertices =
+ FloatArray(actualPoints.size * 2) { ix ->
+ actualPoints[ix / 2].o.let { if (ix % 2 == 0) it.x else it.y }
+ },
+ perVertexRounding = buildList { for (p in actualPoints) add(p.r) },
+ centerX = center.x,
+ centerY = center.y
+ )
+ }
}
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt
index 4cf6819..63959a5 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt
@@ -477,7 +477,7 @@
/** Default size for the leading button end corners and trailing button start corners */
// TODO update token to dp size and use it here
val InnerCornerSize = SplitButtonSmallTokens.InnerCornerSize
- private val InnerCornerSizePressed = ShapeDefaults.CornerSmall
+ private val InnerCornerSizePressed = ShapeDefaults.CornerMedium
/**
* Default percentage size for the leading button start corners and trailing button end corners
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WavyProgressIndicator.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WavyProgressIndicator.kt
index 953dcda..70fea23 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WavyProgressIndicator.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WavyProgressIndicator.kt
@@ -25,7 +25,6 @@
import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
-import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
@@ -284,7 +283,13 @@
* @param stroke a [Stroke] that will be used to draw this indicator
* @param trackStroke a [Stroke] that will be used to draw the indicator's track
* @param gapSize the gap between the track and the progress parts of the indicator
+ * @param amplitude the wave's amplitude. 0.0 represents no amplitude, and 1.0 represents an
+ * amplitude that will take the full height of the progress indicator. Values outside of this
+ * range are coerced into the range.
* @param wavelength the length of a wave
+ * @param waveSpeed the speed in which the wave will move when the [amplitude] is greater than zero.
+ * The value here represents a DP per seconds, and by default it's matched to the [wavelength] to
+ * render an animation that moves the wave by one wave length per second.
* @sample androidx.compose.material3.samples.IndeterminateLinearWavyProgressIndicatorSample
*/
@ExperimentalMaterial3ExpressiveApi
@@ -296,8 +301,41 @@
stroke: Stroke = WavyProgressIndicatorDefaults.linearIndicatorStroke,
trackStroke: Stroke = WavyProgressIndicatorDefaults.linearTrackStroke,
gapSize: Dp = WavyProgressIndicatorDefaults.LinearIndicatorTrackGapSize,
- wavelength: Dp = WavyProgressIndicatorDefaults.LinearIndeterminateWavelength
+ @FloatRange(from = 0.0, to = 1.0) amplitude: Float = 1f,
+ wavelength: Dp = WavyProgressIndicatorDefaults.LinearIndeterminateWavelength,
+ waveSpeed: Dp = wavelength // Match to 1 wavelength per second
) {
+ // Progress offset animation for the waves that is determined by the wave speed and wavelength.
+ val lastOffsetValue = remember { mutableFloatStateOf(0f) }
+ val offsetAnimatable =
+ remember(waveSpeed, wavelength) { Animatable(lastOffsetValue.floatValue) }
+ LaunchedEffect(waveSpeed, wavelength) {
+ if (waveSpeed > 0.dp) {
+ // Compute the duration as a Dp per second.
+ val durationMillis = ((wavelength / waveSpeed) * 1000).toInt()
+ if (durationMillis > 0) {
+ // Update the bounds to start from the current value and to end at the value plus 1.
+ // This will ensure that there are no jumps in the animation in case the wave's
+ // speed or length are changing.
+ offsetAnimatable.updateBounds(
+ lastOffsetValue.floatValue,
+ lastOffsetValue.floatValue + 1
+ )
+ offsetAnimatable.animateTo(
+ lastOffsetValue.floatValue + 1,
+ animationSpec =
+ infiniteRepeatable(
+ animation = tween(durationMillis, easing = LinearEasing),
+ repeatMode = RepeatMode.Restart,
+ )
+ ) {
+ lastOffsetValue.floatValue = value % 1f
+ }
+ }
+ }
+ }
+
+ // Head and tail animation for the two progress lines we will be displaying.
val infiniteTransition = rememberInfiniteTransition()
val firstLineHead =
infiniteTransition.animateFloat(
@@ -324,25 +362,13 @@
animationSpec = linearIndeterminateSecondLineTailAnimationSpec
)
- val waveOffset =
- infiniteTransition.animateFloat(
- 0f,
- 1f,
- infiniteRepeatable(
- animation =
- keyframes {
- durationMillis = LinearAnimationDuration
- 1f at LinearAnimationDuration using LinearIndeterminateProgressEasing
- }
- )
- )
-
// Holds the start and end progress fractions (each two consecutive numbers in the array hold
// the start and the end fractions for a single path)
// In this case of an indeterminate progress indicator, we have 2 progress paths that can
// appear at the same time while the indicator animates.
val progressFractions = floatArrayOf(0f, 0f, 0f, 0f)
val progressDrawingCache = remember { LinearProgressDrawingCache() }
+ val coercedAmplitude = amplitude.coerceIn(0f, 1f)
Spacer(
modifier
.then(IncreaseVerticalSemanticsBounds)
@@ -364,8 +390,8 @@
size = size,
wavelength = wavelength.toPx(),
progressFractions = progressFractions,
- amplitude = 1f,
- waveOffset = (waveOffset.value * 3) % 1f,
+ amplitude = coercedAmplitude,
+ waveOffset = if (coercedAmplitude > 0f) lastOffsetValue.floatValue else 0f,
gapSize = max(0f, gapSize.toPx()),
stroke = stroke,
trackStroke = trackStroke
@@ -485,8 +511,7 @@
progress = progress,
// Resolves the Path from the morph by using the amplitude value as a Morph's progress.
// Ensure that the Path is created with `repeatPath = supportMotion` to allow us to offset
- // the
- // progress later to simulate motion, if enabled.
+ // the progress later to simulate motion, if enabled.
progressPath = {
progressAmplitude,
progressWavelength,
@@ -539,8 +564,16 @@
* @param stroke a [Stroke] that will be used to draw this indicator
* @param trackStroke a [Stroke] that will be used to draw the indicator's track
* @param gapSize the gap between the track and the progress parts of the indicator
+ * @param amplitude the wave's amplitude. 0.0 represents no amplitude, and 1.0 represents an
+ * amplitude that will take the full height of the progress indicator. Values outside of this
+ * range are coerced into the range.
* @param wavelength the length of a wave in this circular indicator. Note that the actual
* wavelength may be different to ensure a continuous wave shape.
+ * @param waveSpeed the speed in which the wave will move when the [amplitude] is greater than zero.
+ * The value here represents a DP per seconds, and by default it's matched to the [wavelength] to
+ * render an animation that moves the wave by one wave length per second. Note that the actual
+ * speed may be slightly different, as the [wavelength] can be adjusted to ensure a continuous
+ * wave shape.
* @sample androidx.compose.material3.samples.IndeterminateCircularWavyProgressIndicatorSample
*/
@ExperimentalMaterial3ExpressiveApi
@@ -552,15 +585,61 @@
stroke: Stroke = WavyProgressIndicatorDefaults.circularIndicatorStroke,
trackStroke: Stroke = WavyProgressIndicatorDefaults.circularTrackStroke,
gapSize: Dp = WavyProgressIndicatorDefaults.CircularIndicatorTrackGapSize,
- wavelength: Dp = WavyProgressIndicatorDefaults.CircularWavelength
+ @FloatRange(from = 0.0, to = 1.0) amplitude: Float = 1f,
+ wavelength: Dp = WavyProgressIndicatorDefaults.CircularWavelength,
+ waveSpeed: Dp = wavelength // Match to 1 wavelength per second
) {
val circularShapes = remember { CircularShapes() }
+ val lastOffsetValue = remember { mutableFloatStateOf(0f) }
+ val offsetAnimatable =
+ remember(waveSpeed, wavelength) { Animatable(lastOffsetValue.floatValue) }
+
+ with(circularShapes) {
+ // Have the LaunchedEffect execute whenever a change in the currentVertexCount state
+ // happens.
+ if (currentVertexCount.intValue >= MinCircularVertexCount) {
+ LaunchedEffect(waveSpeed) {
+ if (waveSpeed > 0.dp) {
+ // Computes the duration as a Dp per second, and take into account the vertex
+ // count. We use the currentVertexCount to compute the duration for the wave's
+ // motion to be as close as possible to the requested speed.
+ val durationMillis =
+ ((wavelength / waveSpeed) * 1000 * currentVertexCount.intValue).toInt()
+ if (durationMillis > 0) {
+ // Update the bounds to start from the current value and to end at the value
+ // plus 1. This will ensure that there are no jumps in the animation in case
+ // the wave's speed or length are changing.
+ offsetAnimatable.updateBounds(
+ lowerBound = lastOffsetValue.floatValue,
+ upperBound = lastOffsetValue.floatValue + 1
+ )
+ offsetAnimatable.animateTo(
+ lastOffsetValue.floatValue + 1,
+ animationSpec =
+ infiniteRepeatable(
+ animation = tween(durationMillis, easing = LinearEasing),
+ repeatMode = RepeatMode.Restart,
+ )
+ ) {
+ lastOffsetValue.floatValue = value % 1f
+ }
+ }
+ }
+ }
+ }
+ }
PathProgressIndicator(
modifier = modifier.size(WavyProgressIndicatorDefaults.CircularContainerSize),
// Resolves the Path from a RoundedPolygon that represents the active indicator.
- progressPath = { _, progressWavelength, strokeWidth, size, _, path ->
+ progressPath = { _, progressWavelength, strokeWidth, size, supportMotion, path ->
circularShapes.update(size, progressWavelength, strokeWidth)
- circularShapes.activeIndicatorPolygon!!.toPath(path)
+ circularShapes.activeIndicatorMorph!!.toPath(
+ progress = amplitude,
+ path = path,
+ repeatPath = supportMotion,
+ rotationPivotX = 0.5f,
+ rotationPivotY = 0.5f,
+ )
},
// Resolves the Path from a RoundedPolygon that represents the track.
trackPath = { _, progressWavelength, strokeWidth, size, path ->
@@ -571,10 +650,13 @@
trackColor = trackColor,
stroke = stroke,
trackStroke = trackStroke,
+ amplitude = amplitude.coerceIn(0f, 1f),
+ waveOffset = { lastOffsetValue.floatValue },
wavelength = wavelength,
gapSize = gapSize,
progressStart = CircularIndeterminateMinProgress,
- progressEnd = CircularIndeterminateMaxProgress
+ progressEnd = CircularIndeterminateMaxProgress,
+ enableProgressMotion = true
)
}
@@ -747,6 +829,12 @@
* @param stroke a [Stroke] that will be used to draw this indicator's progress
* @param trackStroke a [Stroke] that will be used to draw the indicator's track
* @param gapSize the gap between the track and the progress parts of the indicator
+ * @param amplitude the wave's amplitude. 0.0 represents no amplitude, and 1.0 represents an
+ * amplitude that will take the full height of the progress indicator. Values outside of this
+ * range are coerced into the range.
+ * @param waveOffset a lambda that controls the offset of the drawn wave and can be used to apply
+ * motion to the wave. The expected value is between 0.0 to 1.0. Values outside of this range are
+ * coerced into the range. An offset will only be applied when [enableProgressMotion] is true.
* @param wavelength the length of a wave in this circular indicator. Note that the actual
* wavelength may be different to ensure a continuous wave shape.
* @param progressStart the progress value that this indeterminate indicator will start animating
@@ -755,6 +843,10 @@
* @param progressEnd the progress value that this indeterminate indicator will progress towards
* when animating from the [progressStart]. This value is expected to be between 0.0 and 1.0, and
* greater than [progressStart].
+ * @param enableProgressMotion indicates if a progress motion should be enabled for the provided
+ * progress. When enabled, the calls to the [progressPath] will be made with a `supportMotion =
+ * true`, and the generated [Path] will need to be repeated to allow drawing it while shifting the
+ * start and stop points, as well as rotating it, in order to simulate a progress motion.
*/
@Composable
private fun PathProgressIndicator(
@@ -775,9 +867,12 @@
stroke: Stroke,
trackStroke: Stroke,
gapSize: Dp,
+ @FloatRange(from = 0.0, to = 1.0) amplitude: Float,
+ waveOffset: () -> Float,
wavelength: Dp,
@FloatRange(from = 0.0, to = 1.0) progressStart: Float,
- @FloatRange(from = 0.0, to = 1.0) progressEnd: Float
+ @FloatRange(from = 0.0, to = 1.0) progressEnd: Float,
+ enableProgressMotion: Boolean
) {
require(progressEnd > progressStart) {
"Expecting a progress end that is greater than the progress start"
@@ -829,11 +924,16 @@
size = size,
progressPath = progressPath,
trackPath = trackPath,
- enableProgressMotion = false,
+ enableProgressMotion = enableProgressMotion,
startProgress = 0f,
endProgress = progressAnimation.value,
- amplitude = 1f,
- waveOffset = 0f,
+ amplitude = amplitude,
+ waveOffset =
+ if (amplitude > 0f) {
+ waveOffset().coerceIn(0f, 1f)
+ } else {
+ 0f
+ },
wavelength = wavelength.toPx(),
gapSize = trackGapSize,
stroke = stroke,
@@ -980,7 +1080,7 @@
*/
val CircularIndicatorTrackGapSize: Dp = CircularProgressIndicatorTokens.TrackActiveSpace
- /** A function that returns the indicator's amplitude for a given progress */
+ /** A function that returns a determinate indicator's amplitude for a given progress. */
val indicatorAmplitude: (progress: Float) -> Float = { progress ->
// Sets the amplitude to the max on 10%, and back to zero on 95% of the progress.
if (progress <= 0.1f || progress >= 0.95f) {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt
index 09b667b..1bf6f05 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt
@@ -565,7 +565,7 @@
if (anchors.hasAnchorFor(targetValue)) {
try {
dragMutex.mutate(dragPriority) {
- dragTarget = if (confirmValueChange(targetValue)) targetValue else currentValue
+ dragTarget = targetValue
restartable(inputs = { anchors to [email protected] }) {
(latestAnchors, latestTarget) ->
anchoredDragScope.block(latestAnchors, latestTarget)
@@ -856,7 +856,14 @@
} else state.requireOffset()
val xOffset = if (orientation == Orientation.Horizontal) offset else 0f
val yOffset = if (orientation == Orientation.Vertical) offset else 0f
- placeable.place(xOffset.roundToInt(), yOffset.roundToInt())
+ // Tagging as motion frame of reference placement, meaning the placement
+ // contains scrolling. This allows the consumer of this placement offset to
+ // differentiate this offset vs. offsets from structural changes. Generally
+ // speaking, this signals a preference to directly apply changes rather than
+ // animating, to avoid a chasing effect to scrolling.
+ withMotionFrameOfReferencePlacement {
+ placeable.place(xOffset.roundToInt(), yOffset.roundToInt())
+ }
}
}
}
diff --git a/compose/runtime/runtime-lint/build.gradle b/compose/runtime/runtime-lint/build.gradle
index f01441a..d0426fb 100644
--- a/compose/runtime/runtime-lint/build.gradle
+++ b/compose/runtime/runtime-lint/build.gradle
@@ -34,9 +34,9 @@
dependencies {
compileOnly(libs.androidLintApi)
compileOnly(libs.kotlinStdlib)
- bundleInside(projectOrArtifact(":compose:lint:common"))
+ bundleInside(project(":compose:lint:common"))
- testImplementation(projectOrArtifact(":compose:lint:common-test"))
+ testImplementation(project(":compose:lint:common-test"))
testImplementation(libs.kotlinStdlib)
testImplementation(libs.androidLint)
testImplementation(libs.androidLintTests)
diff --git a/compose/runtime/runtime-livedata/build.gradle b/compose/runtime/runtime-livedata/build.gradle
index d82bd53..d42b4b5 100644
--- a/compose/runtime/runtime-livedata/build.gradle
+++ b/compose/runtime/runtime-livedata/build.gradle
@@ -39,7 +39,7 @@
api("androidx.lifecycle:lifecycle-runtime:2.6.1")
api("androidx.lifecycle:lifecycle-runtime-compose:2.8.3")
- androidTestImplementation(projectOrArtifact(":compose:ui:ui-test-junit4"))
+ androidTestImplementation(project(":compose:ui:ui-test-junit4"))
androidTestImplementation(project(":compose:test-utils"))
androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.6.1")
androidTestImplementation(libs.testRunner)
@@ -53,7 +53,7 @@
inceptionYear = "2020"
description = "Compose integration with LiveData"
legacyDisableKotlinStrictApiMode = true
- samples(projectOrArtifact(":compose:runtime:runtime-livedata:runtime-livedata-samples"))
+ samples(project(":compose:runtime:runtime-livedata:runtime-livedata-samples"))
}
android {
diff --git a/compose/runtime/runtime-livedata/samples/build.gradle b/compose/runtime/runtime-livedata/samples/build.gradle
index 80ae202..12e5ab5 100644
--- a/compose/runtime/runtime-livedata/samples/build.gradle
+++ b/compose/runtime/runtime-livedata/samples/build.gradle
@@ -36,7 +36,7 @@
compileOnly(project(":annotation:annotation-sampled"))
implementation("androidx.compose.foundation:foundation:1.2.1")
implementation("androidx.compose.material:material:1.2.1")
- implementation(projectOrArtifact(":compose:runtime:runtime-livedata"))
+ implementation(project(":compose:runtime:runtime-livedata"))
}
androidx {
diff --git a/compose/runtime/runtime-rxjava2/build.gradle b/compose/runtime/runtime-rxjava2/build.gradle
index 5750ab4..1865745 100644
--- a/compose/runtime/runtime-rxjava2/build.gradle
+++ b/compose/runtime/runtime-rxjava2/build.gradle
@@ -38,7 +38,7 @@
api(project(":compose:runtime:runtime"))
api(libs.rxjava2)
- androidTestImplementation(projectOrArtifact(":compose:ui:ui-test-junit4"))
+ androidTestImplementation(project(":compose:ui:ui-test-junit4"))
androidTestImplementation(project(":compose:test-utils"))
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.junit)
@@ -51,7 +51,7 @@
inceptionYear = "2020"
description = "Compose integration with RxJava 2"
legacyDisableKotlinStrictApiMode = true
- samples(projectOrArtifact(":compose:runtime:runtime-rxjava2:runtime-rxjava2-samples"))
+ samples(project(":compose:runtime:runtime-rxjava2:runtime-rxjava2-samples"))
}
android {
diff --git a/compose/runtime/runtime-rxjava2/samples/build.gradle b/compose/runtime/runtime-rxjava2/samples/build.gradle
index 7360519..ba7d0b8 100644
--- a/compose/runtime/runtime-rxjava2/samples/build.gradle
+++ b/compose/runtime/runtime-rxjava2/samples/build.gradle
@@ -36,7 +36,7 @@
compileOnly(project(":annotation:annotation-sampled"))
implementation("androidx.compose.foundation:foundation:1.2.1")
implementation("androidx.compose.material:material:1.2.1")
- implementation(projectOrArtifact(":compose:runtime:runtime-rxjava2"))
+ implementation(project(":compose:runtime:runtime-rxjava2"))
}
androidx {
diff --git a/compose/runtime/runtime-rxjava3/build.gradle b/compose/runtime/runtime-rxjava3/build.gradle
index 7aac897..af420bf 100644
--- a/compose/runtime/runtime-rxjava3/build.gradle
+++ b/compose/runtime/runtime-rxjava3/build.gradle
@@ -38,7 +38,7 @@
api(project(":compose:runtime:runtime"))
api(libs.rxjava3)
- androidTestImplementation(projectOrArtifact(":compose:ui:ui-test-junit4"))
+ androidTestImplementation(project(":compose:ui:ui-test-junit4"))
androidTestImplementation(project(":compose:test-utils"))
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.junit)
@@ -51,7 +51,7 @@
inceptionYear = "2020"
description = "Compose integration with RxJava 3"
legacyDisableKotlinStrictApiMode = true
- samples(projectOrArtifact(":compose:runtime:runtime-rxjava3:runtime-rxjava3-samples"))
+ samples(project(":compose:runtime:runtime-rxjava3:runtime-rxjava3-samples"))
}
android {
diff --git a/compose/runtime/runtime-rxjava3/samples/build.gradle b/compose/runtime/runtime-rxjava3/samples/build.gradle
index 61a0015..9906d22 100644
--- a/compose/runtime/runtime-rxjava3/samples/build.gradle
+++ b/compose/runtime/runtime-rxjava3/samples/build.gradle
@@ -36,7 +36,7 @@
compileOnly(project(":annotation:annotation-sampled"))
implementation("androidx.compose.foundation:foundation:1.2.1")
implementation("androidx.compose.material:material:1.2.1")
- implementation(projectOrArtifact(":compose:runtime:runtime-rxjava3"))
+ implementation(project(":compose:runtime:runtime-rxjava3"))
}
androidx {
diff --git a/compose/runtime/runtime-saveable-lint/build.gradle b/compose/runtime/runtime-saveable-lint/build.gradle
index 24676c6..e911913 100644
--- a/compose/runtime/runtime-saveable-lint/build.gradle
+++ b/compose/runtime/runtime-saveable-lint/build.gradle
@@ -34,9 +34,9 @@
dependencies {
compileOnly(libs.androidLintMinApi)
compileOnly(libs.kotlinStdlib)
- bundleInside(projectOrArtifact(":compose:lint:common"))
+ bundleInside(project(":compose:lint:common"))
- testImplementation(projectOrArtifact(":compose:lint:common-test"))
+ testImplementation(project(":compose:lint:common-test"))
testImplementation(libs.kotlinStdlib)
testImplementation(libs.androidLint)
testImplementation(libs.androidLintTests)
diff --git a/compose/runtime/runtime-test-utils/build.gradle b/compose/runtime/runtime-test-utils/build.gradle
index 62b0876..f5ee280 100644
--- a/compose/runtime/runtime-test-utils/build.gradle
+++ b/compose/runtime/runtime-test-utils/build.gradle
@@ -34,7 +34,7 @@
commonMain {
dependencies {
implementation(libs.kotlinStdlibCommon)
- implementation(projectOrArtifact(":compose:runtime:runtime"))
+ implementation(project(":compose:runtime:runtime"))
implementation kotlin("test")
implementation(libs.kotlinCoroutinesTest)
implementation(libs.kotlinReflect)
diff --git a/compose/runtime/runtime-test-utils/src/commonMain/kotlin/androidx/compose/runtime/mock/ViewApplier.kt b/compose/runtime/runtime-test-utils/src/commonMain/kotlin/androidx/compose/runtime/mock/ViewApplier.kt
index 4b1b684..e92c777 100644
--- a/compose/runtime/runtime-test-utils/src/commonMain/kotlin/androidx/compose/runtime/mock/ViewApplier.kt
+++ b/compose/runtime/runtime-test-utils/src/commonMain/kotlin/androidx/compose/runtime/mock/ViewApplier.kt
@@ -20,6 +20,8 @@
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
class ViewApplier(root: View) : AbstractApplier<View>(root) {
+ var called = false
+
var onBeginChangesCalled = 0
private set
@@ -28,29 +30,53 @@
override fun insertTopDown(index: Int, instance: View) {
// Ignored as the tree is built bottom-up.
+ called = true
}
override fun insertBottomUp(index: Int, instance: View) {
current.addAt(index, instance)
+ called = true
}
override fun remove(index: Int, count: Int) {
current.removeAt(index, count)
+ called = true
}
override fun move(from: Int, to: Int, count: Int) {
current.moveAt(from, to, count)
+ called = true
}
override fun onClear() {
root.removeAllChildren()
+ called = true
}
override fun onBeginChanges() {
onBeginChangesCalled++
+ called = true
}
override fun onEndChanges() {
onEndChangesCalled++
+ called = true
+ }
+
+ override var current: View
+ get() = super.current.also { if (it != root) called = true }
+ set(value) {
+ super.current = value
+ called = true
+ }
+
+ override fun down(node: View) {
+ super.down(node)
+ called = true
+ }
+
+ override fun up() {
+ super.up()
+ called = true
}
}
diff --git a/compose/runtime/runtime/api/current.ignore b/compose/runtime/runtime/api/current.ignore
index fc9bb64..f02ef5a 100644
--- a/compose/runtime/runtime/api/current.ignore
+++ b/compose/runtime/runtime/api/current.ignore
@@ -1,19 +1,3 @@
// Baseline format: 1.0
-AddedAbstractMethod: androidx.compose.runtime.Composer#endReplaceGroup():
- Added method androidx.compose.runtime.Composer.endReplaceGroup()
-AddedAbstractMethod: androidx.compose.runtime.Composer#startReplaceGroup(int):
- Added method androidx.compose.runtime.Composer.startReplaceGroup(int)
-AddedAbstractMethod: androidx.compose.runtime.ControlledComposition#abandonChanges():
- Added method androidx.compose.runtime.ControlledComposition.abandonChanges()
-
-
-InvalidNullConversion: androidx.compose.runtime.ComposablesKt#key(Object[], kotlin.jvm.functions.Function0<? extends T>) parameter #0:
- Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.ComposablesKt.key(Object[] keys, kotlin.jvm.functions.Function0<? extends T> block)
-InvalidNullConversion: androidx.compose.runtime.ComposablesKt#remember(Object[], kotlin.jvm.functions.Function0<? extends T>) parameter #0:
- Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.ComposablesKt.remember(Object[] keys, kotlin.jvm.functions.Function0<? extends T> calculation)
-InvalidNullConversion: androidx.compose.runtime.EffectsKt#DisposableEffect(Object[], kotlin.jvm.functions.Function1<? super androidx.compose.runtime.DisposableEffectScope,? extends androidx.compose.runtime.DisposableEffectResult>) parameter #0:
- Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.EffectsKt.DisposableEffect(Object[] keys, kotlin.jvm.functions.Function1<? super androidx.compose.runtime.DisposableEffectScope,? extends androidx.compose.runtime.DisposableEffectResult> effect)
-InvalidNullConversion: androidx.compose.runtime.EffectsKt#LaunchedEffect(Object[], kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?>) parameter #0:
- Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.EffectsKt.LaunchedEffect(Object[] keys, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block)
-InvalidNullConversion: androidx.compose.runtime.SnapshotStateKt#produceState(T, Object[], kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?>) parameter #1:
- Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.SnapshotStateKt.produceState(T initialValue, Object[] keys, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer)
+AddedAbstractMethod: androidx.compose.runtime.ControlledComposition#setShouldPauseCallback(kotlin.jvm.functions.Function0<java.lang.Boolean>):
+ Added method androidx.compose.runtime.ControlledComposition.setShouldPauseCallback(kotlin.jvm.functions.Function0<java.lang.Boolean>)
diff --git a/compose/runtime/runtime/api/current.txt b/compose/runtime/runtime/api/current.txt
index 110569b..9212ade 100644
--- a/compose/runtime/runtime/api/current.txt
+++ b/compose/runtime/runtime/api/current.txt
@@ -22,6 +22,7 @@
}
@kotlin.jvm.JvmDefaultWithCompatibility public interface Applier<N> {
+ method public default void apply(kotlin.jvm.functions.Function2<? super N,java.lang.Object?,kotlin.Unit> block, Object? value);
method public void clear();
method public void down(N node);
method public N getCurrent();
@@ -31,6 +32,7 @@
method public default void onBeginChanges();
method public default void onEndChanges();
method public void remove(int index, int count);
+ method public default void reuse();
method public void up();
property public abstract N current;
}
@@ -286,6 +288,7 @@
method public void recordModificationsOf(java.util.Set<?> values);
method public void recordReadOf(Object value);
method public void recordWriteOf(Object value);
+ method public kotlin.jvm.functions.Function0<java.lang.Boolean>? setShouldPauseCallback(kotlin.jvm.functions.Function0<java.lang.Boolean>? shouldPause);
method @SuppressCompatibility @androidx.compose.runtime.InternalComposeApi public void verifyConsistent();
property public abstract boolean hasPendingChanges;
property public abstract boolean isComposing;
@@ -459,6 +462,15 @@
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER}) public @interface NonSkippableComposable {
}
+ public interface PausableComposition extends androidx.compose.runtime.ReusableComposition {
+ method public androidx.compose.runtime.PausedComposition setPausableContent(kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method public androidx.compose.runtime.PausedComposition setPausableContentWithReuse(kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ }
+
+ public final class PausableCompositionKt {
+ method public static androidx.compose.runtime.PausableComposition PausableComposition(androidx.compose.runtime.Applier<? extends java.lang.Object?> applier, androidx.compose.runtime.CompositionContext parent);
+ }
+
public final class PausableMonotonicFrameClock implements androidx.compose.runtime.MonotonicFrameClock {
ctor public PausableMonotonicFrameClock(androidx.compose.runtime.MonotonicFrameClock frameClock);
method public boolean isPaused();
@@ -468,6 +480,14 @@
property public final boolean isPaused;
}
+ public interface PausedComposition {
+ method public void apply();
+ method public void cancel();
+ method public boolean isComplete();
+ method public boolean resume(kotlin.jvm.functions.Function0<java.lang.Boolean> shouldPause);
+ property public abstract boolean isComplete;
+ }
+
public final class PrimitiveSnapshotStateKt {
method public static inline operator float getValue(androidx.compose.runtime.FloatState, Object? thisObj, kotlin.reflect.KProperty<? extends java.lang.Object?> property);
method @androidx.compose.runtime.snapshots.StateFactoryMarker public static androidx.compose.runtime.MutableFloatState mutableFloatStateOf(float value);
diff --git a/compose/runtime/runtime/api/restricted_current.ignore b/compose/runtime/runtime/api/restricted_current.ignore
index 5826792..f02ef5a 100644
--- a/compose/runtime/runtime/api/restricted_current.ignore
+++ b/compose/runtime/runtime/api/restricted_current.ignore
@@ -1,23 +1,3 @@
// Baseline format: 1.0
-AddedAbstractMethod: androidx.compose.runtime.Composer#endReplaceGroup():
- Added method androidx.compose.runtime.Composer.endReplaceGroup()
-AddedAbstractMethod: androidx.compose.runtime.Composer#startReplaceGroup(int):
- Added method androidx.compose.runtime.Composer.startReplaceGroup(int)
-AddedAbstractMethod: androidx.compose.runtime.ControlledComposition#abandonChanges():
- Added method androidx.compose.runtime.ControlledComposition.abandonChanges()
-
-
-InvalidNullConversion: androidx.compose.runtime.ComposablesKt#key(Object[], kotlin.jvm.functions.Function0<? extends T>) parameter #0:
- Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.ComposablesKt.key(Object[] keys, kotlin.jvm.functions.Function0<? extends T> block)
-InvalidNullConversion: androidx.compose.runtime.ComposablesKt#remember(Object[], kotlin.jvm.functions.Function0<? extends T>) parameter #0:
- Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.ComposablesKt.remember(Object[] keys, kotlin.jvm.functions.Function0<? extends T> calculation)
-InvalidNullConversion: androidx.compose.runtime.EffectsKt#DisposableEffect(Object[], kotlin.jvm.functions.Function1<? super androidx.compose.runtime.DisposableEffectScope,? extends androidx.compose.runtime.DisposableEffectResult>) parameter #0:
- Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.EffectsKt.DisposableEffect(Object[] keys, kotlin.jvm.functions.Function1<? super androidx.compose.runtime.DisposableEffectScope,? extends androidx.compose.runtime.DisposableEffectResult> effect)
-InvalidNullConversion: androidx.compose.runtime.EffectsKt#LaunchedEffect(Object[], kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?>) parameter #0:
- Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.EffectsKt.LaunchedEffect(Object[] keys, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block)
-InvalidNullConversion: androidx.compose.runtime.SnapshotStateKt#produceState(T, Object[], kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?>) parameter #1:
- Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.SnapshotStateKt.produceState(T initialValue, Object[] keys, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer)
-
-
-RemovedClass: androidx.compose.runtime.ExpectKt:
- Removed class androidx.compose.runtime.ExpectKt
+AddedAbstractMethod: androidx.compose.runtime.ControlledComposition#setShouldPauseCallback(kotlin.jvm.functions.Function0<java.lang.Boolean>):
+ Added method androidx.compose.runtime.ControlledComposition.setShouldPauseCallback(kotlin.jvm.functions.Function0<java.lang.Boolean>)
diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt
index 186ecc4..9580ec3 100644
--- a/compose/runtime/runtime/api/restricted_current.txt
+++ b/compose/runtime/runtime/api/restricted_current.txt
@@ -26,6 +26,7 @@
}
@kotlin.jvm.JvmDefaultWithCompatibility public interface Applier<N> {
+ method public default void apply(kotlin.jvm.functions.Function2<? super N,java.lang.Object?,kotlin.Unit> block, Object? value);
method public void clear();
method public void down(N node);
method public N getCurrent();
@@ -35,6 +36,7 @@
method public default void onBeginChanges();
method public default void onEndChanges();
method public void remove(int index, int count);
+ method public default void reuse();
method public void up();
property public abstract N current;
}
@@ -313,6 +315,7 @@
method public void recordModificationsOf(java.util.Set<?> values);
method public void recordReadOf(Object value);
method public void recordWriteOf(Object value);
+ method public kotlin.jvm.functions.Function0<java.lang.Boolean>? setShouldPauseCallback(kotlin.jvm.functions.Function0<java.lang.Boolean>? shouldPause);
method @SuppressCompatibility @androidx.compose.runtime.InternalComposeApi public void verifyConsistent();
property public abstract boolean hasPendingChanges;
property public abstract boolean isComposing;
@@ -487,6 +490,15 @@
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER}) public @interface NonSkippableComposable {
}
+ public interface PausableComposition extends androidx.compose.runtime.ReusableComposition {
+ method public androidx.compose.runtime.PausedComposition setPausableContent(kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method public androidx.compose.runtime.PausedComposition setPausableContentWithReuse(kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ }
+
+ public final class PausableCompositionKt {
+ method public static androidx.compose.runtime.PausableComposition PausableComposition(androidx.compose.runtime.Applier<? extends java.lang.Object?> applier, androidx.compose.runtime.CompositionContext parent);
+ }
+
public final class PausableMonotonicFrameClock implements androidx.compose.runtime.MonotonicFrameClock {
ctor public PausableMonotonicFrameClock(androidx.compose.runtime.MonotonicFrameClock frameClock);
method public boolean isPaused();
@@ -496,6 +508,14 @@
property public final boolean isPaused;
}
+ public interface PausedComposition {
+ method public void apply();
+ method public void cancel();
+ method public boolean isComplete();
+ method public boolean resume(kotlin.jvm.functions.Function0<java.lang.Boolean> shouldPause);
+ property public abstract boolean isComplete;
+ }
+
public final class PrimitiveSnapshotStateKt {
method public static inline operator float getValue(androidx.compose.runtime.FloatState, Object? thisObj, kotlin.reflect.KProperty<? extends java.lang.Object?> property);
method @androidx.compose.runtime.snapshots.StateFactoryMarker public static androidx.compose.runtime.MutableFloatState mutableFloatStateOf(float value);
diff --git a/compose/runtime/runtime/compose-runtime-benchmark/build.gradle b/compose/runtime/runtime/compose-runtime-benchmark/build.gradle
index 65f7a91..e5794f5 100644
--- a/compose/runtime/runtime/compose-runtime-benchmark/build.gradle
+++ b/compose/runtime/runtime/compose-runtime-benchmark/build.gradle
@@ -28,15 +28,15 @@
dependencies {
- androidTestImplementation(projectOrArtifact(":compose:ui:ui"))
- androidTestImplementation(projectOrArtifact(":compose:ui:ui-test-junit4"))
- androidTestImplementation(projectOrArtifact(":compose:foundation:foundation"))
- androidTestImplementation(projectOrArtifact(":compose:foundation:foundation-layout"))
- androidTestImplementation(projectOrArtifact(":compose:material:material"))
- androidTestImplementation(projectOrArtifact(":compose:runtime:runtime"))
- androidTestImplementation(projectOrArtifact(":compose:runtime:runtime-saveable"))
- androidTestImplementation(projectOrArtifact(":compose:ui:ui-text"))
- androidTestImplementation(projectOrArtifact(":compose:ui:ui-util"))
+ androidTestImplementation(project(":compose:ui:ui"))
+ androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+ androidTestImplementation(project(":compose:foundation:foundation"))
+ androidTestImplementation(project(":compose:foundation:foundation-layout"))
+ androidTestImplementation(project(":compose:material:material"))
+ androidTestImplementation(project(":compose:runtime:runtime"))
+ androidTestImplementation(project(":compose:runtime:runtime-saveable"))
+ androidTestImplementation(project(":compose:ui:ui-text"))
+ androidTestImplementation(project(":compose:ui:ui-util"))
androidTestImplementation(project(":compose:test-utils"))
androidTestImplementation(project(":compose:benchmark-utils"))
@@ -48,9 +48,9 @@
androidTestImplementation(libs.kotlinStdlib)
androidTestImplementation(libs.kotlinReflect)
androidTestImplementation(libs.kotlinCoroutinesTest)
- androidTestImplementation(projectOrArtifact(":benchmark:benchmark-junit4"))
+ androidTestImplementation(project(":benchmark:benchmark-junit4"))
androidTestImplementation("androidx.activity:activity:1.2.0")
- androidTestImplementation(projectOrArtifact(":activity:activity-compose"))
+ androidTestImplementation(project(":activity:activity-compose"))
// old version of common-java8 conflicts with newer version, because both have
// DefaultLifecycleEventObserver.
// Outside of androidx this is resolved via constraint added to lifecycle-common,
diff --git a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmark.kt b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmark.kt
index 7335b2c..d298a52 100644
--- a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmark.kt
+++ b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmark.kt
@@ -17,6 +17,7 @@
package androidx.compose.runtime.benchmark
import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text
@@ -30,8 +31,13 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
+import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.MeasurePolicy
import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.text.drawText
+import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.dp
import androidx.test.annotation.UiThreadTest
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -236,6 +242,21 @@
fun benchmark_f_compose_Rect_100() = runBlockingTestWithFrameClock {
measureComposeFocused { repeat(100) { Rect() } }
}
+
+ @UiThreadTest
+ @Test
+ fun benchmark_g_group_eliding_focused_1000() = runBlockingTestWithFrameClock {
+ measureCompose { repeat(1000) { MyLayout { SimpleText("Value: $it") } } }
+ }
+}
+
+@Composable
+fun MyLayout(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
+ Layout(content = content, measurePolicy = EmptyMeasurePolicy, modifier = modifier)
+}
+
+internal val EmptyMeasurePolicy = MeasurePolicy { _, constraints ->
+ layout(constraints.minWidth, constraints.minHeight) {}
}
class ColorModel(color: Color = Color.Black) {
@@ -254,6 +275,12 @@
}
@Composable
+private fun SimpleText(text: String) {
+ val measurer = rememberTextMeasurer()
+ Box(modifier = Modifier.drawBehind { drawText(measurer, text) })
+}
+
+@Composable
private fun Rect(color: Color) {
val modifier = remember(color) { Modifier.background(color) }
Column(modifier) {}
diff --git a/compose/runtime/runtime/integration-tests/build.gradle b/compose/runtime/runtime/integration-tests/build.gradle
index f4785bb..233a8eb 100644
--- a/compose/runtime/runtime/integration-tests/build.gradle
+++ b/compose/runtime/runtime/integration-tests/build.gradle
@@ -37,7 +37,7 @@
dependencies {
implementation(libs.kotlinStdlibCommon)
implementation(libs.kotlinCoroutinesCore)
- implementation(projectOrArtifact(":compose:ui:ui"))
+ implementation(project(":compose:ui:ui"))
}
}
@@ -74,11 +74,11 @@
androidInstrumentedTest {
dependsOn(jvmTest)
dependencies {
- implementation(projectOrArtifact(":compose:ui:ui"))
- implementation(projectOrArtifact(":compose:material:material"))
- implementation(projectOrArtifact(":compose:ui:ui-test-junit4"))
+ implementation(project(":compose:ui:ui"))
+ implementation(project(":compose:material:material"))
+ implementation(project(":compose:ui:ui-test-junit4"))
implementation(project(":compose:test-utils"))
- implementation(projectOrArtifact(":activity:activity-compose"))
+ implementation(project(":activity:activity-compose"))
implementation(libs.testExtJunit)
implementation(libs.testRules)
implementation(libs.testRunner)
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Applier.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Applier.kt
index e1cd951..5b33661 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Applier.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Applier.kt
@@ -174,6 +174,16 @@
* root to be used as the target of a new composition in the future.
*/
fun clear()
+
+ /** Apply a change to the current node. */
+ fun apply(block: N.(Any?) -> Unit, value: Any?) {
+ current.block(value)
+ }
+
+ /** Notify [current] is is being reused in reusable content. */
+ fun reuse() {
+ (current as? ComposeNodeLifecycleCallback)?.onReuse()
+ }
}
/**
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
index 28f2850..172e582 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
@@ -101,6 +101,15 @@
priority: Int,
endRelativeAfter: Int
)
+
+ /** The restart scope is pausing */
+ fun rememberPausingScope(scope: RecomposeScopeImpl)
+
+ /** The restart scope is resuming */
+ fun startResumingScope(scope: RecomposeScopeImpl)
+
+ /** The restart scope is finished resuming */
+ fun endResumingScope(scope: RecomposeScopeImpl)
}
/**
@@ -1356,6 +1365,9 @@
private var insertAnchor: Anchor = insertTable.read { it.anchor(0) }
private var insertFixups = FixupList()
+ private var pausable: Boolean = false
+ private var shouldPauseCallback: (() -> Boolean)? = null
+
override val applyCoroutineContext: CoroutineContext
@TestOnly get() = parentContext.effectCoroutineContext
@@ -2726,7 +2738,10 @@
providerCache = null
// Invoke the scope's composition function
+ val shouldRestartReusing = !reusing && firstInRange.scope.reusing
+ if (shouldRestartReusing) reusing = true
firstInRange.scope.compose(this)
+ if (shouldRestartReusing) reusing = false
// We could have moved out of a provider so the provider cache is invalid.
providerCache = null
@@ -3038,8 +3053,34 @@
}
@ComposeCompilerApi
- @Suppress("UNUSED")
override fun shouldExecute(parametersChanged: Boolean, flags: Int): Boolean {
+ // We only want to pause when we are not resuming and only when inserting new content or
+ // when reusing content. This 0 bit of `flags` is only 1 if this function was restarted by
+ // the restart lambda. The other bits of this flags are currently all 0's and are reserved
+ // for future use.
+ if (((flags and 1) == 0) && (inserting || reusing)) {
+ val callback = shouldPauseCallback ?: return true
+ val scope = currentRecomposeScope ?: return true
+ val pausing = callback()
+ if (pausing) {
+ scope.used = true
+ // Force the composer back into the reusing state when this scope restarts.
+ scope.reusing = reusing
+ scope.paused = true
+ // Remember a place-holder object to ensure all remembers are sent in the correct
+ // order. The remember manager will record the remember callback for the resumed
+ // content into a place-holder to ensure that, when the remember callbacks are
+ // dispatched, the callbacks for the resumed content are dispatched in the same
+ // order they would have been had the content not paused.
+ changeListWriter.rememberPausingScope(scope)
+ parentContext.reportPausedScope(scope)
+ return false
+ }
+ return true
+ }
+
+ // Otherwise we should execute the function if the parameters have changed or when
+ // skipping is disabled.
return parametersChanged || !skipping
}
@@ -3118,6 +3159,11 @@
}
invalidateStack.push(scope)
scope.start(compositionToken)
+ if (scope.paused) {
+ scope.paused = false
+ scope.resuming = true
+ changeListWriter.startResumingScope(scope)
+ }
}
}
@@ -3133,8 +3179,16 @@
// exception stack unwinding that might have not called the doneJoin/endRestartGroup in the
// the correct order.
val scope = if (invalidateStack.isNotEmpty()) invalidateStack.pop() else null
- scope?.requiresRecompose = false
- scope?.end(compositionToken)?.let { changeListWriter.endCompositionScope(it, composition) }
+ if (scope != null) {
+ scope.requiresRecompose = false
+ scope.end(compositionToken)?.let {
+ changeListWriter.endCompositionScope(it, composition)
+ }
+ if (scope.resuming) {
+ scope.resuming = false
+ changeListWriter.endResumingScope(scope)
+ }
+ }
val result =
if (scope != null && !scope.skipped && (scope.used || forceRecomposeScopes)) {
if (scope.anchor == null) {
@@ -3438,10 +3492,16 @@
*/
internal fun composeContent(
invalidationsRequested: ScopeMap<RecomposeScopeImpl, Any>,
- content: @Composable () -> Unit
+ content: @Composable () -> Unit,
+ shouldPause: (() -> Boolean)?
) {
runtimeCheck(changes.isEmpty()) { "Expected applyChanges() to have been called" }
- doCompose(invalidationsRequested, content)
+ this.shouldPauseCallback = shouldPause
+ try {
+ doCompose(invalidationsRequested, content)
+ } finally {
+ this.shouldPauseCallback = null
+ }
}
internal fun prepareCompose(block: () -> Unit) {
@@ -3460,6 +3520,7 @@
*/
internal fun recompose(
invalidationsRequested: ScopeMap<RecomposeScopeImpl, Any>,
+ shouldPause: (() -> Boolean)?
): Boolean {
runtimeCheck(changes.isEmpty()) { "Expected applyChanges() to have been called" }
// even if invalidationsRequested is empty we still need to recompose if the Composer has
@@ -3467,7 +3528,12 @@
// there were a change for a state which was used by the child composition. such changes
// will be tracked and added into `invalidations` list.
if (invalidationsRequested.size > 0 || invalidations.isNotEmpty() || forciblyRecompose) {
- doCompose(invalidationsRequested, null)
+ shouldPauseCallback = shouldPause
+ try {
+ doCompose(invalidationsRequested, null)
+ } finally {
+ shouldPauseCallback = null
+ }
return changes.isNotEmpty()
}
return false
@@ -3786,6 +3852,10 @@
parentContext.unregisterComposition(composition)
}
+ override fun reportPausedScope(scope: RecomposeScopeImpl) {
+ parentContext.reportPausedScope(scope)
+ }
+
override val effectCoroutineContext: CoroutineContext
get() = parentContext.effectCoroutineContext
@@ -3802,6 +3872,20 @@
parentContext.composeInitial(composition, content)
}
+ override fun composeInitialPaused(
+ composition: ControlledComposition,
+ shouldPause: () -> Boolean,
+ content: @Composable () -> Unit
+ ): ScatterSet<RecomposeScopeImpl> =
+ parentContext.composeInitialPaused(composition, shouldPause, content)
+
+ override fun recomposePaused(
+ composition: ControlledComposition,
+ shouldPause: () -> Boolean,
+ invalidScopes: ScatterSet<RecomposeScopeImpl>
+ ): ScatterSet<RecomposeScopeImpl> =
+ parentContext.recomposePaused(composition, shouldPause, invalidScopes)
+
override fun invalidate(composition: ControlledComposition) {
// Invalidate ourselves with our parent before we invalidate a child composer.
// This ensures that when we are scheduling recompositions, parents always
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
index e2efd82..f674dbe 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
@@ -18,13 +18,12 @@
package androidx.compose.runtime
-import androidx.collection.MutableIntList
import androidx.collection.MutableScatterSet
-import androidx.collection.mutableScatterSetOf
import androidx.compose.runtime.changelist.ChangeList
import androidx.compose.runtime.collection.ScopeMap
import androidx.compose.runtime.collection.fastForEach
import androidx.compose.runtime.internal.AtomicReference
+import androidx.compose.runtime.internal.RememberEventDispatcher
import androidx.compose.runtime.internal.trace
import androidx.compose.runtime.snapshots.ReaderKind
import androidx.compose.runtime.snapshots.StateObjectImpl
@@ -289,6 +288,29 @@
* used to compose as if the scopes have already been changed.
*/
fun <R> delegateInvalidations(to: ControlledComposition?, groupIndex: Int, block: () -> R): R
+
+ /**
+ * Sets the [shouldPause] callback allowing a composition to be pausable if it is not `null`.
+ * Setting the callback to `null` disables pausing.
+ *
+ * @return the previous value of the callback which will be restored once the callback is no
+ * longer needed.
+ * @see PausableComposition
+ */
+ fun setShouldPauseCallback(shouldPause: (() -> Boolean)?): (() -> Boolean)?
+}
+
+/** Utility function to set and restore a should pause callback. */
+internal inline fun <R> ControlledComposition.pausable(
+ noinline shouldPause: () -> Boolean,
+ block: () -> R
+): R {
+ val previous = setShouldPauseCallback(shouldPause)
+ return try {
+ block()
+ } finally {
+ setShouldPauseCallback(previous)
+ }
}
/**
@@ -409,7 +431,12 @@
/** The applier to use to update the tree managed by the composition. */
private val applier: Applier<*>,
recomposeContext: CoroutineContext? = null
-) : ControlledComposition, ReusableComposition, RecomposeScopeOwner, CompositionServices {
+) :
+ ControlledComposition,
+ ReusableComposition,
+ RecomposeScopeOwner,
+ CompositionServices,
+ PausableComposition {
/**
* `null` if a composition isn't pending to apply. `Set<Any>` or `Array<Set<Any>>` if there are
* modifications to record [PendingApplyNoModifications] if a composition is pending to apply,
@@ -520,6 +547,14 @@
@Suppress("MemberVisibilityCanBePrivate") // published as internal
internal var pendingInvalidScopes = false
+ /**
+ * If the [shouldPause] callback is set the composition is pausable and should pause whenever
+ * the [shouldPause] callback returns `true`.
+ */
+ private var shouldPause: (() -> Boolean)? = null
+
+ private var pendingPausedComposition: PausedCompositionImpl? = null
+
private var invalidationDelegate: CompositionImpl? = null
private var invalidationDelegateGroup: Int = 0
@@ -572,10 +607,16 @@
get() = synchronized(lock) { composer.hasPendingChanges }
override fun setContent(content: @Composable () -> Unit) {
+ checkPrecondition(pendingPausedComposition == null) {
+ "A pausable composition is in progress"
+ }
composeInitial(content)
}
override fun setContentWithReuse(content: @Composable () -> Unit) {
+ checkPrecondition(pendingPausedComposition == null) {
+ "A pausable composition is in progress"
+ }
composer.startReuseFromRoot()
composeInitial(content)
@@ -583,6 +624,50 @@
composer.endReuseFromRoot()
}
+ override fun setPausableContent(content: @Composable () -> Unit): PausedComposition {
+ checkPrecondition(!disposed) { "The composition is disposed" }
+ checkPrecondition(pendingPausedComposition == null) {
+ "A pausable composition is in progress"
+ }
+ val pausedComposition =
+ PausedCompositionImpl(
+ composition = this,
+ context = parent,
+ composer = composer,
+ content = content,
+ reusable = false,
+ abandonSet = abandonSet,
+ applier = applier,
+ lock = lock,
+ )
+ pendingPausedComposition = pausedComposition
+ return pausedComposition
+ }
+
+ override fun setPausableContentWithReuse(content: @Composable () -> Unit): PausedComposition {
+ checkPrecondition(!disposed) { "The composition is disposed" }
+ checkPrecondition(pendingPausedComposition == null) {
+ "A pausable composition is in progress"
+ }
+ val pausedComposition =
+ PausedCompositionImpl(
+ composition = this,
+ context = parent,
+ composer = composer,
+ content = content,
+ reusable = true,
+ abandonSet = abandonSet,
+ applier = applier,
+ lock = lock,
+ )
+ pendingPausedComposition = pausedComposition
+ return pausedComposition
+ }
+
+ internal fun pausedCompositionFinished() {
+ pendingPausedComposition = null
+ }
+
private fun composeInitial(content: @Composable () -> Unit) {
checkPrecondition(!disposed) { "The composition is disposed" }
this.composable = content
@@ -701,7 +786,7 @@
invalidations.asMap() as Map<RecomposeScope, Set<Any>>
)
}
- composer.composeContent(invalidations, content)
+ composer.composeContent(invalidations, content, shouldPause)
observer?.onEndComposition(this)
}
}
@@ -911,7 +996,7 @@
this,
invalidations.asMap() as Map<RecomposeScope, Set<Any>>
)
- composer.recompose(invalidations).also { shouldDrain ->
+ composer.recompose(invalidations, shouldPause).also { shouldDrain ->
// Apply would normally do this for us; do it now if apply shouldn't happen.
if (!shouldDrain) drainPendingModificationsLocked()
observer?.onEndComposition(this)
@@ -939,11 +1024,13 @@
try {
if (changes.isEmpty()) return
trace("Compose:applyChanges") {
+ val applier = pendingPausedComposition?.pausableApplier ?: applier
+ val rememberManager = pendingPausedComposition?.rememberManager ?: manager
applier.onBeginChanges()
// Apply all changes
slotTable.write { slots ->
- changes.executeAndFlushAllPendingChanges(applier, slots, manager)
+ changes.executeAndFlushAllPendingChanges(applier, slots, rememberManager)
}
applier.onEndChanges()
}
@@ -962,9 +1049,12 @@
}
}
} finally {
- // Only dispatch abandons if we do not have any late changes. The instances in the
- // abandon set can be remembered in the late changes.
- if (this.lateChanges.isEmpty()) manager.dispatchAbandons()
+ // Only dispatch abandons if we do not have any late changes or pending paused
+ // compositions. The instances in the abandon set can be remembered in the late changes
+ // or when the paused composition is applied.
+ if (this.lateChanges.isEmpty() && pendingPausedComposition == null) {
+ manager.dispatchAbandons()
+ }
}
}
@@ -1062,6 +1152,12 @@
} else block()
}
+ override fun setShouldPauseCallback(shouldPause: (() -> Boolean)?): (() -> Boolean)? {
+ val previous = this.shouldPause
+ this.shouldPause = shouldPause
+ return previous
+ }
+
override fun invalidate(scope: RecomposeScopeImpl, instance: Any?): InvalidationResult {
if (scope.defaultsInScope) {
scope.defaultsInvalid = true
@@ -1241,218 +1337,6 @@
// This is only used in tests to ensure the stacks do not silently leak.
internal fun composerStacksSizes(): Int = composer.stacksSize()
-
- /** Helper for collecting remember observers for later strictly ordered dispatch. */
- private class RememberEventDispatcher(private val abandoning: MutableSet<RememberObserver>) :
- RememberManager {
- private val remembering = mutableListOf<RememberObserver>()
- private val leaving = mutableListOf<Any>()
- private val sideEffects = mutableListOf<() -> Unit>()
- private var releasing: MutableScatterSet<ComposeNodeLifecycleCallback>? = null
- private val pending = mutableListOf<Any>()
- private val priorities = MutableIntList()
- private val afters = MutableIntList()
-
- override fun remembering(instance: RememberObserver) {
- remembering.add(instance)
- }
-
- override fun forgetting(
- instance: RememberObserver,
- endRelativeOrder: Int,
- priority: Int,
- endRelativeAfter: Int
- ) {
- recordLeaving(instance, endRelativeOrder, priority, endRelativeAfter)
- }
-
- override fun sideEffect(effect: () -> Unit) {
- sideEffects += effect
- }
-
- override fun deactivating(
- instance: ComposeNodeLifecycleCallback,
- endRelativeOrder: Int,
- priority: Int,
- endRelativeAfter: Int
- ) {
- recordLeaving(instance, endRelativeOrder, priority, endRelativeAfter)
- }
-
- override fun releasing(
- instance: ComposeNodeLifecycleCallback,
- endRelativeOrder: Int,
- priority: Int,
- endRelativeAfter: Int
- ) {
- val releasing =
- releasing
- ?: mutableScatterSetOf<ComposeNodeLifecycleCallback>().also { releasing = it }
-
- releasing += instance
- recordLeaving(instance, endRelativeOrder, priority, endRelativeAfter)
- }
-
- fun dispatchRememberObservers() {
- // Add any pending out-of-order forgotten objects
- processPendingLeaving(Int.MIN_VALUE)
-
- // Send forgets and node callbacks
- if (leaving.isNotEmpty()) {
- trace("Compose:onForgotten") {
- val releasing = releasing
- for (i in leaving.size - 1 downTo 0) {
- val instance = leaving[i]
- if (instance is RememberObserver) {
- abandoning.remove(instance)
- instance.onForgotten()
- }
- if (instance is ComposeNodeLifecycleCallback) {
- // node callbacks are in the same queue as forgets to ensure ordering
- if (releasing != null && instance in releasing) {
- instance.onRelease()
- } else {
- instance.onDeactivate()
- }
- }
- }
- }
- }
-
- // Send remembers
- if (remembering.isNotEmpty()) {
- trace("Compose:onRemembered") {
- remembering.fastForEach { instance ->
- abandoning.remove(instance)
- instance.onRemembered()
- }
- }
- }
- }
-
- fun dispatchSideEffects() {
- if (sideEffects.isNotEmpty()) {
- trace("Compose:sideeffects") {
- sideEffects.fastForEach { sideEffect -> sideEffect() }
- sideEffects.clear()
- }
- }
- }
-
- fun dispatchAbandons() {
- if (abandoning.isNotEmpty()) {
- trace("Compose:abandons") {
- val iterator = abandoning.iterator()
- // remove elements one by one to ensure that abandons will not be dispatched
- // second time in case [onAbandoned] throws.
- while (iterator.hasNext()) {
- val instance = iterator.next()
- iterator.remove()
- instance.onAbandoned()
- }
- }
- }
- }
-
- private fun recordLeaving(
- instance: Any,
- endRelativeOrder: Int,
- priority: Int,
- endRelativeAfter: Int
- ) {
- processPendingLeaving(endRelativeOrder)
- if (endRelativeAfter in 0 until endRelativeOrder) {
- pending.add(instance)
- priorities.add(priority)
- afters.add(endRelativeAfter)
- } else {
- leaving.add(instance)
- }
- }
-
- private fun processPendingLeaving(endRelativeOrder: Int) {
- if (pending.isNotEmpty()) {
- var index = 0
- var toAdd: MutableList<Any>? = null
- var toAddAfter: MutableIntList? = null
- var toAddPriority: MutableIntList? = null
- while (index < afters.size) {
- if (endRelativeOrder <= afters[index]) {
- val instance = pending.removeAt(index)
- val endRelativeAfter = afters.removeAt(index)
- val priority = priorities.removeAt(index)
-
- if (toAdd == null) {
- toAdd = mutableListOf(instance)
- toAddAfter = MutableIntList().also { it.add(endRelativeAfter) }
- toAddPriority = MutableIntList().also { it.add(priority) }
- } else {
- toAddPriority as MutableIntList
- toAddAfter as MutableIntList
- toAdd.add(instance)
- toAddAfter.add(endRelativeAfter)
- toAddPriority.add(priority)
- }
- } else {
- index++
- }
- }
- if (toAdd != null) {
- toAddPriority as MutableIntList
- toAddAfter as MutableIntList
-
- // Sort the list into [after, -priority] order where it is ordered by after
- // in ascending order as the primary key and priority in descending order as
- // secondary key.
-
- // For example if remember occurs after a child group it must be added after
- // all the remembers of the child. This is reported with an after which is the
- // slot index of the child's last slot. As this slot might be at the same
- // location as where its parents ends this would be ambiguous which should
- // first if both the two groups request a slot to be after the same slot.
- // Priority is used to break the tie here which is the group index of the group
- // which is leaving. Groups that are lower must be added before the parent's
- // remember when they have the same after.
-
- // The sort must be stable as as consecutive remembers in the same group after
- // the same child will have the same after and priority.
-
- // A selection sort is used here because it is stable and the groups are
- // typically very short so this quickly exit list of one and not loop for
- // for sizes of 2. As the information is split between three lists, to
- // reduce allocations, [MutableList.sort] cannot be used as it doesn't have
- // an option to supply a custom swap.
- for (i in 0 until toAdd.size - 1) {
- for (j in i + 1 until toAdd.size) {
- val iAfter = toAddAfter[i]
- val jAfter = toAddAfter[j]
- if (
- iAfter < jAfter ||
- (jAfter == iAfter && toAddPriority[i] < toAddPriority[j])
- ) {
- toAdd.swap(i, j)
- toAddPriority.swap(i, j)
- toAddAfter.swap(i, j)
- }
- }
- }
- leaving.addAll(toAdd)
- }
- }
- }
- }
-}
-
-private fun <T> MutableList<T>.swap(a: Int, b: Int) {
- val item = this[a]
- this[a] = this[b]
- this[b] = item
-}
-
-private fun MutableIntList.swap(a: Int, b: Int) {
- val item = this[a]
- this[a] = this[b]
- this[b] = item
}
internal object ScopeInvalidated
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionContext.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionContext.kt
index 561890c..e5b6d6b 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionContext.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionContext.kt
@@ -16,6 +16,7 @@
package androidx.compose.runtime
+import androidx.collection.ScatterSet
import androidx.compose.runtime.internal.persistentCompositionLocalHashMapOf
import androidx.compose.runtime.tooling.CompositionData
import kotlin.coroutines.CoroutineContext
@@ -52,6 +53,20 @@
content: @Composable () -> Unit
)
+ internal abstract fun composeInitialPaused(
+ composition: ControlledComposition,
+ shouldPause: () -> Boolean,
+ content: @Composable () -> Unit
+ ): ScatterSet<RecomposeScopeImpl>
+
+ internal abstract fun recomposePaused(
+ composition: ControlledComposition,
+ shouldPause: () -> Boolean,
+ invalidScopes: ScatterSet<RecomposeScopeImpl>
+ ): ScatterSet<RecomposeScopeImpl>
+
+ internal abstract fun reportPausedScope(scope: RecomposeScopeImpl)
+
internal abstract fun invalidate(composition: ControlledComposition)
internal abstract fun invalidateScope(scope: RecomposeScopeImpl)
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/PausableComposition.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/PausableComposition.kt
new file mode 100644
index 0000000..0eff8d6
--- /dev/null
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/PausableComposition.kt
@@ -0,0 +1,381 @@
+/*
+ * 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(InternalComposeApi::class)
+
+package androidx.compose.runtime
+
+import androidx.collection.emptyScatterSet
+import androidx.collection.mutableIntListOf
+import androidx.collection.mutableObjectListOf
+import androidx.compose.runtime.internal.RememberEventDispatcher
+
+/**
+ * A [PausableComposition] is a sub-composition that can be composed incrementally as it supports
+ * being paused and resumed.
+ *
+ * Pausable sub-composition can be used between frames to prepare a sub-composition before it is
+ * required by the main composition. For example, this is used in lazy lists to prepare list items
+ * in between frames to that are likely to be scrolled in. The composition is paused when the start
+ * of the next frame is near allowing composition to be spread across multiple frames without
+ * delaying the production of the next frame.
+ *
+ * The result of the composition should not be used (e.g. the nodes should not added to a layout
+ * tree or placed in layout) until [PausedComposition.isComplete] is `true` and
+ * [PausedComposition.apply] has been called. The composition is incomplete and will not
+ * automatically recompose until after [PausedComposition.apply] is called.
+ *
+ * A [PausableComposition] is a [ReusableComposition] but [setPausableContent] should be used
+ * instead of [ReusableComposition.setContentWithReuse] to create a paused composition.
+ *
+ * If [Composition.setContent] or [ReusableComposition.setContentWithReuse] are used then the
+ * composition behaves as if it wasn't pausable. If there is a [PausedComposition] that has not yet
+ * been applied, an exception is thrown.
+ *
+ * @see Composition
+ * @see ReusableComposition
+ */
+interface PausableComposition : ReusableComposition {
+ /**
+ * Set the content of the composition. A [PausedComposition] that is currently paused. No
+ * composition is performed until [PausedComposition.resume] is called.
+ * [PausedComposition.resume] should be called until [PausedComposition.isComplete] is `true`.
+ * The composition should not be used until [PausedComposition.isComplete] is `true` and
+ * [PausedComposition.apply] has been called.
+ *
+ * @see Composition.setContent
+ * @see ReusableComposition.setContentWithReuse
+ */
+ fun setPausableContent(content: @Composable () -> Unit): PausedComposition
+
+ /**
+ * Set the content of a resuable composition. A [PausedComposition] that is currently paused. No
+ * composition is performed until [PausedComposition.resume] is called.
+ * [PausedComposition.resume] should be called until [PausedComposition.isComplete] is `true`.
+ * The composition should not be used until [PausedComposition.isComplete] is `true` and
+ * [PausedComposition.apply] has been called.
+ *
+ * @see Composition.setContent
+ * @see ReusableComposition.setContentWithReuse
+ */
+ fun setPausableContentWithReuse(content: @Composable () -> Unit): PausedComposition
+}
+
+/**
+ * [PausedComposition] is the result of calling [PausableComposition.setContent] or
+ * [PausableComposition.setContentWithReuse]. It is used to drive the paused composition to
+ * completion. A [PausedComposition] should not be used until [isComplete] is `true` and [apply] has
+ * been called.
+ *
+ * A [PausedComposition] is created paused and will only compose the `content` parameter when
+ * [resume] is called the first time.
+ */
+interface PausedComposition {
+ /**
+ * Returns `true` when the [PausedComposition] is complete. [isComplete] matches the last value
+ * returned from [resume]. Once a [PausedComposition] is [isComplete] the [apply] method should
+ * be called.
+ */
+ val isComplete: Boolean
+
+ /**
+ * Resume the composition that has been paused. This method should be called until [resume]
+ * returns `true` or [isComplete] is `true` which has the same result as the last result of
+ * calling [resume]. The [shouldPause] parameter is a lambda that returns whether the
+ * composition should be paused. For example, in lazy lists this returns `false` until just
+ * prior to the next frame starting in which it returns `true`
+ *
+ * Calling [resume] after it returns `true` or when `isComplete` is true will throw an
+ * exception.
+ *
+ * @param shouldPause A lambda that is used to determine if the composition should be paused.
+ * This lambda is called often so should be a very simple calculation. Returning `true` does
+ * not guarantee the composition will pause, it should only be considered a request to pause
+ * the composition. Not all composable functions are pausable and only pausable composition
+ * functions will pause.
+ * @return `true` if the composition is complete and `false` if one or more calls to `resume`
+ * are required to complete composition.
+ */
+ fun resume(shouldPause: () -> Boolean): Boolean
+
+ /**
+ * Apply the composition. This is the last step of a paused composition and is required to be
+ * called prior to the composition is usable.
+ */
+ fun apply()
+
+ /**
+ * Cancels the paused composition. This should only be used if the composition is going to be
+ * disposed and the entire composition is not going to be used.
+ */
+ fun cancel()
+}
+
+/**
+ * Create a [PausableComposition]. A [PausableComposition] can create a [PausedComposition] which
+ * allows pausing and resuming the composition.
+ *
+ * @param applier The [Applier] instance to be used in the composition.
+ * @param parent The parent [CompositionContext].
+ * @see Applier
+ * @see CompositionContext
+ * @see PausableComposition
+ */
+fun PausableComposition(applier: Applier<*>, parent: CompositionContext): PausableComposition =
+ CompositionImpl(parent, applier)
+
+internal enum class PausedCompositionState {
+ Invalid,
+ Cancelled,
+ InitialPending,
+ RecomposePending,
+ ApplyPending,
+ Applied,
+}
+
+internal class PausedCompositionImpl(
+ val composition: CompositionImpl,
+ val context: CompositionContext,
+ val composer: ComposerImpl,
+ abandonSet: MutableSet<RememberObserver>,
+ val content: @Composable () -> Unit,
+ val reusable: Boolean,
+ val applier: Applier<*>,
+ val lock: SynchronizedObject,
+) : PausedComposition {
+ private var state = PausedCompositionState.InitialPending
+ private var invalidScopes = emptyScatterSet<RecomposeScopeImpl>()
+ internal val rememberManager = RememberEventDispatcher(abandonSet)
+ internal val pausableApplier = RecordingApplier(applier.current)
+
+ override val isComplete: Boolean
+ get() = state >= PausedCompositionState.ApplyPending
+
+ override fun resume(shouldPause: () -> Boolean): Boolean {
+ try {
+ when (state) {
+ PausedCompositionState.InitialPending -> {
+ if (reusable) composer.startReuseFromRoot()
+ try {
+ invalidScopes =
+ context.composeInitialPaused(composition, shouldPause, content)
+ } finally {
+ if (reusable) composer.endReuseFromRoot()
+ }
+ state = PausedCompositionState.RecomposePending
+ if (invalidScopes.isEmpty()) markComplete()
+ }
+ PausedCompositionState.RecomposePending -> {
+ invalidScopes = context.recomposePaused(composition, shouldPause, invalidScopes)
+ if (invalidScopes.isEmpty()) markComplete()
+ }
+ PausedCompositionState.ApplyPending ->
+ error("Pausable composition is complete and apply() should be applied")
+ PausedCompositionState.Applied -> error("The paused composition has been applied")
+ PausedCompositionState.Cancelled ->
+ error("The paused composition has been cancelled")
+ PausedCompositionState.Invalid ->
+ error("The paused composition is invalid because of a previous exception")
+ }
+ } catch (e: Exception) {
+ state = PausedCompositionState.Invalid
+ }
+ return isComplete
+ }
+
+ override fun apply() {
+ try {
+ when (state) {
+ PausedCompositionState.InitialPending,
+ PausedCompositionState.RecomposePending ->
+ error("The paused composition has not completed yet")
+ PausedCompositionState.ApplyPending -> {
+ applyChanges()
+ state = PausedCompositionState.Applied
+ }
+ PausedCompositionState.Applied ->
+ error("The paused composition has already been applied")
+ PausedCompositionState.Cancelled ->
+ error("The paused composition has been cancelled")
+ PausedCompositionState.Invalid ->
+ error("The paused composition is invalid because of a previous exception")
+ }
+ } catch (e: Exception) {
+ state = PausedCompositionState.Invalid
+ throw e
+ }
+ }
+
+ override fun cancel() {
+ state = PausedCompositionState.Cancelled
+ rememberManager.dispatchAbandons()
+ composition.pausedCompositionFinished()
+ }
+
+ private fun markComplete() {
+ state = PausedCompositionState.ApplyPending
+ }
+
+ private fun applyChanges() {
+ synchronized(lock) {
+ @Suppress("UNCHECKED_CAST")
+ try {
+ pausableApplier.playTo(applier as Applier<Any?>)
+ rememberManager.dispatchRememberObservers()
+ rememberManager.dispatchSideEffects()
+ } finally {
+ rememberManager.dispatchAbandons()
+ composition.pausedCompositionFinished()
+ }
+ }
+ }
+}
+
+internal class RecordingApplier<N>(root: N) : Applier<N> {
+ private val stack = mutableObjectListOf<N>()
+ private val operations = mutableIntListOf()
+ private val instances = mutableObjectListOf<Any?>()
+
+ override var current: N = root
+
+ override fun down(node: N) {
+ operations.add(DOWN)
+ instances.add(node)
+ stack.add(current)
+ current = node
+ }
+
+ override fun up() {
+ operations.add(UP)
+ current = stack.removeAt(stack.size - 1)
+ }
+
+ override fun remove(index: Int, count: Int) {
+ operations.add(REMOVE)
+ operations.add(index)
+ operations.add(count)
+ }
+
+ override fun move(from: Int, to: Int, count: Int) {
+ operations.add(MOVE)
+ operations.add(from)
+ operations.add(to)
+ operations.add(count)
+ }
+
+ override fun clear() {
+ operations.add(CLEAR)
+ }
+
+ override fun insertBottomUp(index: Int, instance: N) {
+ operations.add(INSERT_BOTTOM_UP)
+ operations.add(index)
+ instances.add(instance)
+ }
+
+ override fun insertTopDown(index: Int, instance: N) {
+ operations.add(INSERT_TOP_DOWN)
+ operations.add(index)
+ instances.add(instance)
+ }
+
+ override fun apply(block: N.(Any?) -> Unit, value: Any?) {
+ operations.add(APPLY)
+ instances.add(block)
+ instances.add(value)
+ }
+
+ override fun reuse() {
+ operations.add(REUSE)
+ }
+
+ fun playTo(applier: Applier<N>) {
+ var currentOperation = 0
+ var currentInstance = 0
+ val operations = operations
+ val size = operations.size
+ val instances = instances
+ applier.onBeginChanges()
+ try {
+ while (currentOperation < size) {
+ val operation = operations[currentOperation++]
+ when (operation) {
+ UP -> {
+ applier.up()
+ }
+ DOWN -> {
+ @Suppress("UNCHECKED_CAST") val node = instances[currentInstance++] as N
+ applier.down(node)
+ }
+ REMOVE -> {
+ val index = operations[currentOperation++]
+ val count = operations[currentOperation++]
+ applier.remove(index, count)
+ }
+ MOVE -> {
+ val from = operations[currentOperation++]
+ val to = operations[currentOperation++]
+ val count = operations[currentOperation++]
+ applier.move(from, to, count)
+ }
+ CLEAR -> {
+ applier.clear()
+ }
+ INSERT_TOP_DOWN -> {
+ val index = operations[currentOperation++]
+
+ @Suppress("UNCHECKED_CAST") val instance = instances[currentInstance++] as N
+ applier.insertTopDown(index, instance)
+ }
+ INSERT_BOTTOM_UP -> {
+ val index = operations[currentOperation++]
+
+ @Suppress("UNCHECKED_CAST") val instance = instances[currentInstance++] as N
+ applier.insertBottomUp(index, instance)
+ }
+ APPLY -> {
+ @Suppress("UNCHECKED_CAST")
+ val block = instances[currentInstance++] as Any?.(Any?) -> Unit
+ val value = instances[currentInstance++]
+ applier.apply(block, value)
+ }
+ REUSE -> {
+ applier.reuse()
+ }
+ }
+ }
+ runtimeCheck(currentInstance == instances.size) { "Applier operation size mismatch" }
+ instances.clear()
+ operations.clear()
+ } finally {
+ applier.onEndChanges()
+ }
+ }
+
+ // These commands need to be an integer, not just a enum value, as they are stored along side
+ // the commands integer parameters, so the values are explicitly set.
+ companion object {
+ const val UP = 0
+ const val DOWN = UP + 1
+ const val REMOVE = DOWN + 1
+ const val MOVE = REMOVE + 1
+ const val CLEAR = MOVE + 1
+ const val INSERT_BOTTOM_UP = CLEAR + 1
+ const val INSERT_TOP_DOWN = INSERT_BOTTOM_UP + 1
+ const val APPLY = INSERT_TOP_DOWN + 1
+ const val REUSE = APPLY + 1
+ }
+}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt
index 88245a7..48657d5 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt
@@ -55,13 +55,16 @@
((lowBits shl 1) and highBits))
}
-private const val UsedFlag = 0x01
-private const val DefaultsInScopeFlag = 0x02
-private const val DefaultsInvalidFlag = 0x04
-private const val RequiresRecomposeFlag = 0x08
-private const val SkippedFlag = 0x10
-private const val RereadingFlag = 0x20
-private const val ForcedRecomposeFlag = 0x40
+private const val UsedFlag = 0x001
+private const val DefaultsInScopeFlag = 0x002
+private const val DefaultsInvalidFlag = 0x004
+private const val RequiresRecomposeFlag = 0x008
+private const val SkippedFlag = 0x010
+private const val RereadingFlag = 0x020
+private const val ForcedRecomposeFlag = 0x040
+private const val ForceReusing = 0x080
+private const val Paused = 0x100
+private const val Resuming = 0x200
internal interface RecomposeScopeOwner {
fun invalidate(scope: RecomposeScopeImpl, instance: Any?): InvalidationResult
@@ -110,11 +113,51 @@
var used: Boolean
get() = flags and UsedFlag != 0
set(value) {
- if (value) {
- flags = flags or UsedFlag
- } else {
- flags = flags and UsedFlag.inv()
- }
+ flags =
+ if (value) {
+ flags or UsedFlag
+ } else {
+ flags and UsedFlag.inv()
+ }
+ }
+
+ /**
+ * Used to force a scope to the reusing state when a composition is paused while reusing
+ * content.
+ */
+ var reusing: Boolean
+ get() = flags and ForceReusing != 0
+ set(value) {
+ flags =
+ if (value) {
+ flags or ForceReusing
+ } else {
+ flags and ForceReusing.inv()
+ }
+ }
+
+ /** Used to flag a scope as paused for pausable compositions */
+ var paused: Boolean
+ get() = flags and Paused != 0
+ set(value) {
+ flags =
+ if (value) {
+ flags or Paused
+ } else {
+ flags and Paused.inv()
+ }
+ }
+
+ /** Used to flag a scope as paused for pausable compositions */
+ var resuming: Boolean
+ get() = flags and Resuming != 0
+ set(value) {
+ flags =
+ if (value) {
+ flags or Resuming
+ } else {
+ flags and Resuming.inv()
+ }
}
/**
@@ -299,7 +342,9 @@
}
fun scopeSkipped() {
- skipped = true
+ if (!reusing) {
+ skipped = true
+ }
}
/**
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
index 67d0d8a..e347076 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
@@ -17,12 +17,15 @@
package androidx.compose.runtime
import androidx.collection.MutableScatterSet
+import androidx.collection.ScatterSet
+import androidx.collection.emptyScatterSet
import androidx.collection.mutableScatterSetOf
import androidx.compose.runtime.collection.fastForEach
import androidx.compose.runtime.collection.mutableVectorOf
import androidx.compose.runtime.collection.wrapIntoSet
import androidx.compose.runtime.external.kotlinx.collections.immutable.persistentSetOf
import androidx.compose.runtime.internal.AtomicReference
+import androidx.compose.runtime.internal.SnapshotThreadLocal
import androidx.compose.runtime.internal.logError
import androidx.compose.runtime.internal.trace
import androidx.compose.runtime.snapshots.MutableSnapshot
@@ -232,6 +235,7 @@
// End properties guarded by stateLock
private val _state = MutableStateFlow(State.Inactive)
+ private val pausedScopes = SnapshotThreadLocal<MutableScatterSet<RecomposeScopeImpl>?>()
/**
* A [Job] used as a parent of any effects created by this [Recomposer]'s compositions. Its
@@ -1116,6 +1120,54 @@
}
}
+ internal override fun composeInitialPaused(
+ composition: ControlledComposition,
+ shouldPause: () -> Boolean,
+ content: @Composable () -> Unit
+ ): ScatterSet<RecomposeScopeImpl> {
+ return try {
+ composition.pausable(shouldPause) {
+ composeInitial(composition, content)
+ pausedScopes.get() ?: emptyScatterSet()
+ }
+ } finally {
+ pausedScopes.set(null)
+ }
+ }
+
+ internal override fun recomposePaused(
+ composition: ControlledComposition,
+ shouldPause: () -> Boolean,
+ invalidScopes: ScatterSet<RecomposeScopeImpl>
+ ): ScatterSet<RecomposeScopeImpl> {
+ return try {
+ recordComposerModifications()
+ composition.recordModificationsOf(invalidScopes.wrapIntoSet())
+ composition.pausable(shouldPause) {
+ val needsApply = performRecompose(composition, null)
+ if (needsApply != null) {
+ performInitialMovableContentInserts(composition)
+ needsApply.applyChanges()
+ needsApply.applyLateChanges()
+ }
+ pausedScopes.get() ?: emptyScatterSet()
+ }
+ } finally {
+ pausedScopes.set(null)
+ }
+ }
+
+ override fun reportPausedScope(scope: RecomposeScopeImpl) {
+ val scopes =
+ pausedScopes.get()
+ ?: run {
+ val newScopes = mutableScatterSetOf<RecomposeScopeImpl>()
+ pausedScopes.set(newScopes)
+ newScopes
+ }
+ scopes.add(scope)
+ }
+
private fun performInitialMovableContentInserts(composition: ControlledComposition) {
synchronized(stateLock) {
if (!compositionValuesAwaitingInsert.fastAny { it.composition == composition }) return
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
index cce9013..fe4b799 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
@@ -18,6 +18,7 @@
import androidx.collection.MutableIntObjectMap
import androidx.collection.MutableIntSet
+import androidx.collection.MutableObjectList
import androidx.compose.runtime.snapshots.fastAny
import androidx.compose.runtime.snapshots.fastFilterIndexed
import androidx.compose.runtime.snapshots.fastForEach
@@ -1290,6 +1291,12 @@
/** This a count of the [nodeCount] of the explicitly started groups. */
private val nodeCountStack = IntStack()
+ /**
+ * Deferred slot writes for open groups to avoid thrashing the slot table when slots are added
+ * to parent group which already has children.
+ */
+ private var deferredSlotWrites: MutableIntObjectMap<MutableObjectList<Any?>>? = null
+
/** The current group that will be started by [startGroup] or skipped by [skipGroup] */
var currentGroup = 0
private set
@@ -1439,6 +1446,19 @@
* being inserted.
*/
fun update(value: Any?): Any? {
+ if (insertCount > 0 && currentSlot != slotsGapStart) {
+ // Defer write as doing it now would thrash the slot table.
+ val deferred =
+ (deferredSlotWrites ?: MutableIntObjectMap())
+ .also { deferredSlotWrites = it }
+ .getOrPut(parent) { MutableObjectList() }
+ deferred.add(value)
+ return Composer.Empty
+ }
+ return rawUpdate(value)
+ }
+
+ private fun rawUpdate(value: Any?): Any? {
val result = skip()
set(value)
return result
@@ -1664,7 +1684,7 @@
groups.dataIndex(groupIndexToAddress(groupIndex + groupSize(groupIndex)))
private val currentGroupSlotIndex: Int
- get() = currentSlot - slotsStartIndex(parent)
+ get() = currentSlot - slotsStartIndex(parent) + (deferredSlotWrites?.get(parent)?.size ?: 0)
/**
* Advance [currentGroup] by [amount]. The [currentGroup] group cannot be advanced outside the
@@ -1850,6 +1870,14 @@
val newGroupSize = currentGroup - groupIndex
val isNode = groups.isNode(groupAddress)
if (inserting) {
+ // Check for deferred slot writes
+ val deferredSlotWrites = deferredSlotWrites
+ deferredSlotWrites?.get(groupIndex)?.let {
+ it.forEach { value -> rawUpdate(value) }
+ deferredSlotWrites.remove(groupIndex)
+ }
+
+ // Close the group
groups.updateGroupSize(groupAddress, newGroupSize)
groups.updateNodeCount(groupAddress, newNodes)
nodeCount = nodeCountStack.pop() + if (isNode) 1 else newNodes
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/ChangeList.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/ChangeList.kt
index 4780cdb..e2eaa76 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/ChangeList.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/ChangeList.kt
@@ -25,6 +25,7 @@
import androidx.compose.runtime.InternalComposeApi
import androidx.compose.runtime.MovableContentState
import androidx.compose.runtime.MovableContentStateReference
+import androidx.compose.runtime.RecomposeScopeImpl
import androidx.compose.runtime.RememberManager
import androidx.compose.runtime.RememberObserver
import androidx.compose.runtime.SlotTable
@@ -40,6 +41,7 @@
import androidx.compose.runtime.changelist.Operation.EndCompositionScope
import androidx.compose.runtime.changelist.Operation.EndCurrentGroup
import androidx.compose.runtime.changelist.Operation.EndMovableContentPlacement
+import androidx.compose.runtime.changelist.Operation.EndResumingScope
import androidx.compose.runtime.changelist.Operation.EnsureGroupStarted
import androidx.compose.runtime.changelist.Operation.EnsureRootGroupStarted
import androidx.compose.runtime.changelist.Operation.InsertSlots
@@ -48,11 +50,13 @@
import androidx.compose.runtime.changelist.Operation.MoveNode
import androidx.compose.runtime.changelist.Operation.ReleaseMovableGroupAtCurrent
import androidx.compose.runtime.changelist.Operation.Remember
+import androidx.compose.runtime.changelist.Operation.RememberPausingScope
import androidx.compose.runtime.changelist.Operation.RemoveCurrentGroup
import androidx.compose.runtime.changelist.Operation.RemoveNode
import androidx.compose.runtime.changelist.Operation.ResetSlots
import androidx.compose.runtime.changelist.Operation.SideEffect
import androidx.compose.runtime.changelist.Operation.SkipToEndOfCurrentGroup
+import androidx.compose.runtime.changelist.Operation.StartResumingScope
import androidx.compose.runtime.changelist.Operation.TrimParentValues
import androidx.compose.runtime.changelist.Operation.UpdateAnchoredValue
import androidx.compose.runtime.changelist.Operation.UpdateAuxData
@@ -87,6 +91,18 @@
operations.push(Remember) { setObject(Remember.Value, value) }
}
+ fun pushRememberPausingScope(scope: RecomposeScopeImpl) {
+ operations.push(RememberPausingScope) { setObject(RememberPausingScope.Scope, scope) }
+ }
+
+ fun pushStartResumingScope(scope: RecomposeScopeImpl) {
+ operations.push(StartResumingScope) { setObject(StartResumingScope.Scope, scope) }
+ }
+
+ fun pushEndResumingScope(scope: RecomposeScopeImpl) {
+ operations.push(EndResumingScope) { setObject(EndResumingScope.Scope, scope) }
+ }
+
fun pushUpdateValue(value: Any?, groupSlotIndex: Int) {
operations.push(UpdateValue) {
setObject(UpdateValue.Value, value)
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/ComposerChangeListWriter.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/ComposerChangeListWriter.kt
index 74c7146..9b87d45 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/ComposerChangeListWriter.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/ComposerChangeListWriter.kt
@@ -25,6 +25,7 @@
import androidx.compose.runtime.InternalComposeApi
import androidx.compose.runtime.MovableContentState
import androidx.compose.runtime.MovableContentStateReference
+import androidx.compose.runtime.RecomposeScopeImpl
import androidx.compose.runtime.RememberObserver
import androidx.compose.runtime.SlotReader
import androidx.compose.runtime.SlotTable
@@ -192,6 +193,18 @@
changeList.pushRemember(value)
}
+ fun rememberPausingScope(scope: RecomposeScopeImpl) {
+ changeList.pushRememberPausingScope(scope)
+ }
+
+ fun startResumingScope(scope: RecomposeScopeImpl) {
+ changeList.pushStartResumingScope(scope)
+ }
+
+ fun endResumingScope(scope: RecomposeScopeImpl) {
+ changeList.pushEndResumingScope(scope)
+ }
+
fun updateValue(value: Any?, groupSlotIndex: Int) {
pushSlotTableOperationPreamble(useParentSlot = true)
changeList.pushUpdateValue(value, groupSlotIndex)
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operation.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operation.kt
index 894f3d47..15aa234 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operation.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operation.kt
@@ -18,7 +18,6 @@
import androidx.compose.runtime.Anchor
import androidx.compose.runtime.Applier
-import androidx.compose.runtime.ComposeNodeLifecycleCallback
import androidx.compose.runtime.Composition
import androidx.compose.runtime.CompositionContext
import androidx.compose.runtime.ControlledComposition
@@ -171,6 +170,66 @@
}
}
+ object RememberPausingScope : Operation(objects = 1) {
+ inline val Scope
+ get() = ObjectParameter<RecomposeScopeImpl>(0)
+
+ override fun objectParamName(parameter: ObjectParameter<*>): String =
+ when (parameter) {
+ Scope -> "scope"
+ else -> super.objectParamName(parameter)
+ }
+
+ override fun OperationArgContainer.execute(
+ applier: Applier<*>,
+ slots: SlotWriter,
+ rememberManager: RememberManager
+ ) {
+ val scope = getObject(Scope)
+ rememberManager.rememberPausingScope(scope)
+ }
+ }
+
+ object StartResumingScope : Operation(objects = 1) {
+ inline val Scope
+ get() = ObjectParameter<RecomposeScopeImpl>(0)
+
+ override fun objectParamName(parameter: ObjectParameter<*>): String =
+ when (parameter) {
+ Scope -> "scope"
+ else -> super.objectParamName(parameter)
+ }
+
+ override fun OperationArgContainer.execute(
+ applier: Applier<*>,
+ slots: SlotWriter,
+ rememberManager: RememberManager
+ ) {
+ val scope = getObject(Scope)
+ rememberManager.startResumingScope(scope)
+ }
+ }
+
+ object EndResumingScope : Operation(objects = 1) {
+ inline val Scope
+ get() = ObjectParameter<RecomposeScopeImpl>(0)
+
+ override fun objectParamName(parameter: ObjectParameter<*>): String =
+ when (parameter) {
+ Scope -> "scope"
+ else -> super.objectParamName(parameter)
+ }
+
+ override fun OperationArgContainer.execute(
+ applier: Applier<*>,
+ slots: SlotWriter,
+ rememberManager: RememberManager
+ ) {
+ val scope = getObject(Scope)
+ rememberManager.endResumingScope(scope)
+ }
+ }
+
object AppendValue : Operation(objects = 2) {
inline val Anchor
get() = ObjectParameter<Anchor>(0)
@@ -467,7 +526,7 @@
slots: SlotWriter,
rememberManager: RememberManager
) {
- (applier.current as ComposeNodeLifecycleCallback).onReuse()
+ applier.reuse()
}
}
@@ -492,7 +551,7 @@
) {
val value = getObject(Value)
val block = getObject(Block)
- applier.current.block(value)
+ applier.apply(block, value)
}
}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/RememberEventDispatcher.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/RememberEventDispatcher.kt
new file mode 100644
index 0000000..d9d78ea
--- /dev/null
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/RememberEventDispatcher.kt
@@ -0,0 +1,301 @@
+/*
+ * 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.runtime.internal
+
+import androidx.collection.MutableIntList
+import androidx.collection.MutableScatterMap
+import androidx.collection.MutableScatterSet
+import androidx.collection.mutableScatterMapOf
+import androidx.collection.mutableScatterSetOf
+import androidx.compose.runtime.ComposeNodeLifecycleCallback
+import androidx.compose.runtime.RecomposeScopeImpl
+import androidx.compose.runtime.RememberManager
+import androidx.compose.runtime.RememberObserver
+import androidx.compose.runtime.Stack
+import androidx.compose.runtime.snapshots.fastForEach
+
+/**
+ * Used as a placeholder for paused compositions to ensure the remembers are dispatch in the correct
+ * order. While the paused composition is resuming all remembered objects are placed into the this
+ * classes list instead of the main list. As remembers are dispatched, this will dispatch remembers
+ * to the object remembered in the paused composition's content in the order that they would have
+ * been dispatched had the composition not been paused.
+ */
+internal class PausedCompositionRemembers(private val abandoning: MutableSet<RememberObserver>) :
+ RememberObserver {
+ val pausedRemembers = mutableListOf<RememberObserver>()
+
+ override fun onRemembered() {
+ pausedRemembers.fastForEach {
+ abandoning.remove(it)
+ it.onRemembered()
+ }
+ }
+
+ // These are never called
+ override fun onForgotten() {}
+
+ override fun onAbandoned() {}
+}
+
+/** Helper for collecting remember observers for later strictly ordered dispatch. */
+internal class RememberEventDispatcher(private val abandoning: MutableSet<RememberObserver>) :
+ RememberManager {
+ private val remembering = mutableListOf<RememberObserver>()
+ private var currentRememberingList = remembering
+ private val leaving = mutableListOf<Any>()
+ private val sideEffects = mutableListOf<() -> Unit>()
+ private var releasing: MutableScatterSet<ComposeNodeLifecycleCallback>? = null
+ private var pausedPlaceholders:
+ MutableScatterMap<RecomposeScopeImpl, PausedCompositionRemembers>? =
+ null
+ private val pending = mutableListOf<Any>()
+ private val priorities = MutableIntList()
+ private val afters = MutableIntList()
+ private var nestedRemembersLists: Stack<MutableList<RememberObserver>>? = null
+
+ override fun remembering(instance: RememberObserver) {
+ currentRememberingList.add(instance)
+ }
+
+ override fun forgetting(
+ instance: RememberObserver,
+ endRelativeOrder: Int,
+ priority: Int,
+ endRelativeAfter: Int
+ ) {
+ recordLeaving(instance, endRelativeOrder, priority, endRelativeAfter)
+ }
+
+ override fun sideEffect(effect: () -> Unit) {
+ sideEffects += effect
+ }
+
+ override fun deactivating(
+ instance: ComposeNodeLifecycleCallback,
+ endRelativeOrder: Int,
+ priority: Int,
+ endRelativeAfter: Int
+ ) {
+ recordLeaving(instance, endRelativeOrder, priority, endRelativeAfter)
+ }
+
+ override fun releasing(
+ instance: ComposeNodeLifecycleCallback,
+ endRelativeOrder: Int,
+ priority: Int,
+ endRelativeAfter: Int
+ ) {
+ val releasing =
+ releasing ?: mutableScatterSetOf<ComposeNodeLifecycleCallback>().also { releasing = it }
+
+ releasing += instance
+ recordLeaving(instance, endRelativeOrder, priority, endRelativeAfter)
+ }
+
+ override fun rememberPausingScope(scope: RecomposeScopeImpl) {
+ val pausedPlaceholder = PausedCompositionRemembers(abandoning)
+ (pausedPlaceholders
+ ?: mutableScatterMapOf<RecomposeScopeImpl, PausedCompositionRemembers>().also {
+ pausedPlaceholders = it
+ })[scope] = pausedPlaceholder
+ this.currentRememberingList.add(pausedPlaceholder)
+ }
+
+ override fun startResumingScope(scope: RecomposeScopeImpl) {
+ val placeholder = pausedPlaceholders?.get(scope)
+ if (placeholder != null) {
+ (nestedRemembersLists
+ ?: Stack<MutableList<RememberObserver>>().also { nestedRemembersLists = it })
+ .push(currentRememberingList)
+ currentRememberingList = placeholder.pausedRemembers
+ }
+ }
+
+ override fun endResumingScope(scope: RecomposeScopeImpl) {
+ val pausedPlaceholders = pausedPlaceholders
+ if (pausedPlaceholders != null) {
+ val placeholder = pausedPlaceholders[scope]
+ if (placeholder != null) {
+ nestedRemembersLists?.pop()?.let { currentRememberingList = it }
+ pausedPlaceholders.remove(scope)
+ }
+ }
+ }
+
+ fun dispatchRememberObservers() {
+ // Add any pending out-of-order forgotten objects
+ processPendingLeaving(Int.MIN_VALUE)
+
+ // Send forgets and node callbacks
+ if (leaving.isNotEmpty()) {
+ trace("Compose:onForgotten") {
+ val releasing = releasing
+ for (i in leaving.size - 1 downTo 0) {
+ val instance = leaving[i]
+ if (instance is RememberObserver) {
+ abandoning.remove(instance)
+ instance.onForgotten()
+ }
+ if (instance is ComposeNodeLifecycleCallback) {
+ // node callbacks are in the same queue as forgets to ensure ordering
+ if (releasing != null && instance in releasing) {
+ instance.onRelease()
+ } else {
+ instance.onDeactivate()
+ }
+ }
+ }
+ }
+ }
+
+ // Send remembers
+ if (remembering.isNotEmpty()) {
+ trace("Compose:onRemembered") { dispatchRememberList(remembering) }
+ }
+ }
+
+ private fun dispatchRememberList(list: List<RememberObserver>) {
+ list.fastForEach { instance ->
+ abandoning.remove(instance)
+ instance.onRemembered()
+ }
+ }
+
+ fun dispatchSideEffects() {
+ if (sideEffects.isNotEmpty()) {
+ trace("Compose:sideeffects") {
+ sideEffects.fastForEach { sideEffect -> sideEffect() }
+ sideEffects.clear()
+ }
+ }
+ }
+
+ fun dispatchAbandons() {
+ if (abandoning.isNotEmpty()) {
+ trace("Compose:abandons") {
+ val iterator = abandoning.iterator()
+ // remove elements one by one to ensure that abandons will not be dispatched
+ // second time in case [onAbandoned] throws.
+ while (iterator.hasNext()) {
+ val instance = iterator.next()
+ iterator.remove()
+ instance.onAbandoned()
+ }
+ }
+ }
+ }
+
+ private fun recordLeaving(
+ instance: Any,
+ endRelativeOrder: Int,
+ priority: Int,
+ endRelativeAfter: Int
+ ) {
+ processPendingLeaving(endRelativeOrder)
+ if (endRelativeAfter in 0 until endRelativeOrder) {
+ pending.add(instance)
+ priorities.add(priority)
+ afters.add(endRelativeAfter)
+ } else {
+ leaving.add(instance)
+ }
+ }
+
+ private fun processPendingLeaving(endRelativeOrder: Int) {
+ if (pending.isNotEmpty()) {
+ var index = 0
+ var toAdd: MutableList<Any>? = null
+ var toAddAfter: MutableIntList? = null
+ var toAddPriority: MutableIntList? = null
+ while (index < afters.size) {
+ if (endRelativeOrder <= afters[index]) {
+ val instance = pending.removeAt(index)
+ val endRelativeAfter = afters.removeAt(index)
+ val priority = priorities.removeAt(index)
+
+ if (toAdd == null) {
+ toAdd = mutableListOf(instance)
+ toAddAfter = MutableIntList().also { it.add(endRelativeAfter) }
+ toAddPriority = MutableIntList().also { it.add(priority) }
+ } else {
+ toAddPriority as MutableIntList
+ toAddAfter as MutableIntList
+ toAdd.add(instance)
+ toAddAfter.add(endRelativeAfter)
+ toAddPriority.add(priority)
+ }
+ } else {
+ index++
+ }
+ }
+ if (toAdd != null) {
+ toAddPriority as MutableIntList
+ toAddAfter as MutableIntList
+
+ // Sort the list into [after, -priority] order where it is ordered by after
+ // in ascending order as the primary key and priority in descending order as
+ // secondary key.
+
+ // For example if remember occurs after a child group it must be added after
+ // all the remembers of the child. This is reported with an after which is the
+ // slot index of the child's last slot. As this slot might be at the same
+ // location as where its parents ends this would be ambiguous which should
+ // first if both the two groups request a slot to be after the same slot.
+ // Priority is used to break the tie here which is the group index of the group
+ // which is leaving. Groups that are lower must be added before the parent's
+ // remember when they have the same after.
+
+ // The sort must be stable as as consecutive remembers in the same group after
+ // the same child will have the same after and priority.
+
+ // A selection sort is used here because it is stable and the groups are
+ // typically very short so this quickly exit list of one and not loop for
+ // for sizes of 2. As the information is split between three lists, to
+ // reduce allocations, [MutableList.sort] cannot be used as it doesn't have
+ // an option to supply a custom swap.
+ for (i in 0 until toAdd.size - 1) {
+ for (j in i + 1 until toAdd.size) {
+ val iAfter = toAddAfter[i]
+ val jAfter = toAddAfter[j]
+ if (
+ iAfter < jAfter ||
+ (jAfter == iAfter && toAddPriority[i] < toAddPriority[j])
+ ) {
+ toAdd.swap(i, j)
+ toAddPriority.swap(i, j)
+ toAddAfter.swap(i, j)
+ }
+ }
+ }
+ leaving.addAll(toAdd)
+ }
+ }
+ }
+}
+
+private fun <T> MutableList<T>.swap(a: Int, b: Int) {
+ val item = this[a]
+ this[a] = this[b]
+ this[b] = item
+}
+
+private fun MutableIntList.swap(a: Int, b: Int) {
+ val item = this[a]
+ this[a] = this[b]
+ this[b] = item
+}
diff --git a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionTests.kt b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
index c2560dd..2d820aa 100644
--- a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
+++ b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
@@ -4608,6 +4608,19 @@
revalidate()
}
+ @Test // regression test for b/362291064
+ fun avoidsThrashingTheSlotTable() = compositionTest {
+ val count = 100
+ var data by mutableIntStateOf(0)
+ compose { repeat(count) { Linear { Text("Value: $it, data: $data") } } }
+
+ validate { repeat(count) { Linear { Text("Value: $it, data: $data") } } }
+
+ data++
+ advance()
+ revalidate()
+ }
+
private inline fun CoroutineScope.withGlobalSnapshotManager(block: CoroutineScope.() -> Unit) {
val channel = Channel<Unit>(Channel.CONFLATED)
val job = launch { channel.consumeEach { Snapshot.sendApplyNotifications() } }
@@ -4800,16 +4813,16 @@
private val rob_reports_to_alice = Report("Rob", "Alice")
private val clark_reports_to_lois = Report("Clark", "Lois")
-private interface Counted {
+internal interface Counted {
val count: Int
}
-private interface Ordered {
+internal interface Ordered {
val rememberOrder: Int
val forgetOrder: Int
}
-private interface Named {
+internal interface Named {
val name: String
}
diff --git a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/PausableCompositionTests.kt b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/PausableCompositionTests.kt
new file mode 100644
index 0000000..36a9747
--- /dev/null
+++ b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/PausableCompositionTests.kt
@@ -0,0 +1,606 @@
+/*
+ * 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.runtime
+
+import androidx.compose.runtime.mock.EmptyApplier
+import androidx.compose.runtime.mock.Linear
+import androidx.compose.runtime.mock.MockViewValidator
+import androidx.compose.runtime.mock.Text
+import androidx.compose.runtime.mock.View
+import androidx.compose.runtime.mock.ViewApplier
+import androidx.compose.runtime.mock.compositionTest
+import androidx.compose.runtime.mock.validate
+import androidx.compose.runtime.mock.view
+import kotlin.coroutines.resume
+import kotlin.test.Ignore
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import kotlinx.coroutines.CancellableContinuation
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.test.runTest
+
+@Stable
+class PausableCompositionTests {
+ @Test
+ fun canCreateARootPausableComposition() = runTest {
+ val recomposer = Recomposer(coroutineContext)
+ val pausableComposition = PausableComposition(EmptyApplier(), recomposer)
+ pausableComposition.dispose()
+ recomposer.cancel()
+ recomposer.close()
+ }
+
+ @Test
+ fun canCreateANestedPausableComposition() = compositionTest {
+ compose {
+ val parent = rememberCompositionContext()
+ DisposableEffect(Unit) {
+ val pausableComposition = PausableComposition(EmptyApplier(), parent)
+ onDispose { pausableComposition.dispose() }
+ }
+ }
+ }
+
+ @Test
+ fun canRecordAComposition() = compositionTest {
+ // This just tests the recording mechanism used in the tests below.
+ val recording = recordTest {
+ compose { A() }
+
+ validate { this.A() }
+ }
+
+ // Legend for the recording:
+ // +N: Enter N for functions A, B, C, D, (where A:1 is the first lambda in A())
+ // -N: Exit N
+ // *N: Calling N (e.g *B is recorded before B() is called).
+ // ^n: calling remember for some value
+
+ // Here we expect the normal, synchronous, execution as the recorded composition is not
+ // pausable. That is if we see a *B that should immediately followed by a B+ its content and
+ // a B-.
+ assertEquals(
+ recording,
+ "+A, ^z, ^Y, *B, +B, *Linear, +A:1, *C, +C, ^x, *Text, -C, *D, +D, +D:1, *C, +C, " +
+ "^x, *Text, -C, *C, +C, ^x, *Text, -C, *C, +C, ^x, *Text, -C, -D:1, -D, -A:1, " +
+ "-B, -A"
+ )
+ }
+
+ @Test
+ @Ignore // Requires compiler support
+ fun canPauseContent() = compositionTest {
+ val awaiter = Awaiter()
+ var receivedIteration = 0
+ val recording = recordTest {
+ compose {
+ PausableContent(
+ normalWorkflow {
+ receivedIteration = iteration
+ awaiter.done()
+ }
+ ) {
+ A()
+ }
+ }
+ awaiter.await()
+ }
+ validate { this.PausableContent { this.A() } }
+ assertEquals(10, receivedIteration)
+
+ // Same Legend as canRecordAComposition
+ // Here we expect all functions to exit before the content of the function is executed
+ // because the above will pause at every pause point. If we see a B* we should not receive
+ // a B+ until after the caller finishes. (e.g. A-).
+ assertEquals(
+ recording,
+ "+A, ^z, ^Y, *B, -A, +B, *Linear, -B, +A:1, *C, *D, -A:1, +C, " +
+ "^x, *Text, -C, +D, -D, +D:1, *C, *C, *C, -D:1, +C, ^x, *Text, -C, +C, ^x, *Text, " +
+ "-C, +C, ^x, *Text, -C"
+ )
+ }
+
+ @Test
+ @Ignore // Requires compiler support
+ fun canPauseReusableContent() = compositionTest {
+ val awaiter = Awaiter()
+ var receivedIteration = 0
+ val recording = recordTest {
+ compose {
+ PausableContent(
+ reuseWorkflow {
+ receivedIteration = iteration
+ awaiter.done()
+ }
+ ) {
+ A()
+ }
+ }
+ awaiter.await()
+ }
+ validate { this.PausableContent { this.A() } }
+ assertEquals(10, receivedIteration)
+ // Same Legend as canRecordAComposition
+ // Here we expect the result to be the same as if we were inserting new content as in
+ // canPauseContent
+ assertEquals(
+ "+A, ^z, ^Y, *B, -A, +B, *Linear, -B, +A:1, *C, *D, -A:1, +C, " +
+ "^x, *Text, -C, +D, -D, +D:1, *C, *C, *C, -D:1, +C, ^x, *Text, -C, +C, ^x, *Text, " +
+ "-C, +C, ^x, *Text, -C",
+ recording
+ )
+ }
+
+ @Test
+ @Ignore // Requires compiler support
+ fun canPauseReusingContent() = compositionTest {
+ val awaiter = Awaiter()
+ var recording = ""
+ val workflow: Workflow = {
+ // Create the content
+ setContentWithReuse()
+ resumeTillComplete { false }
+ apply()
+
+ // Reuse the content
+ recording = recordTest {
+ setContentWithReuse()
+ resumeTillComplete { true }
+ apply()
+ }
+ awaiter.done()
+ }
+
+ compose { PausableContent(workflow) { A() } }
+ awaiter.await()
+ // Same Legend as canRecordAComposition
+ // Here we expect the result to be the same as if we were inserting new content as in
+ // canPauseContent
+ assertArrayEquals(
+ ("+A, ^z, ^Y, *B, -A, +B, *Linear, -B, +A:1, *C, *D, -A:1, +C, " +
+ "^x, *Text, -C, +D, -D, +D:1, *C, *C, *C, -D:1, +C, ^x, *Text, -C, +C, ^x, *Text, " +
+ "-C, +C, ^x, *Text, -C")
+ .splitRecording(),
+ recording.splitRecording()
+ )
+ }
+
+ @Test
+ fun applierOnlyCalledInApply() = compositionTest {
+ val awaiter = Awaiter()
+ var applier: ViewApplier? = null
+
+ val workflow = workflow {
+ setContent()
+
+ assertFalse(applier?.called == true, "Applier was called during set content")
+
+ resumeTillComplete { false }
+
+ assertFalse(applier?.called == true, "Applier was called during resume")
+
+ apply()
+
+ assertTrue(applier?.called == true, "Applier wasn't called")
+
+ awaiter.done()
+ }
+
+ compose {
+ PausableContent(workflow, { view -> ViewApplier(view).also { applier = it } }) { A() }
+ }
+ awaiter.await()
+ }
+
+ @Test
+ @Ignore // Requires compiler support
+ fun rememberOnlyCalledInApply() = compositionTest {
+ val awaiter = Awaiter()
+ var onRememberCalled = false
+
+ val workflow = workflow {
+ setContent()
+ assertFalse(onRememberCalled, "onRemember called during set content")
+
+ resumeTillComplete {
+ assertFalse(onRememberCalled, "onRemember called during resume")
+ true
+ }
+ assertFalse(onRememberCalled, "onRemember called before resume returned")
+
+ apply()
+
+ assertTrue(onRememberCalled, "onRemember was not called in apply")
+
+ awaiter.done()
+ }
+
+ fun rememberedObject(name: String) =
+ object : RememberObserver {
+ val name = name
+
+ override fun onRemembered() {
+ onRememberCalled = true
+ report("+$name")
+ }
+
+ override fun onForgotten() {
+ report("-$name")
+ }
+
+ override fun onAbandoned() {
+ report("!$name")
+ }
+ }
+
+ val recording = recordTest {
+ compose {
+ PausableContent(workflow) {
+ val a = remember { rememberedObject("a") }
+ report("C(${a.name})")
+ B {
+ val b = remember { rememberedObject("b") }
+ report("C(${b.name})")
+ B {
+ val c = remember { rememberedObject("c") }
+ report("C(${c.name})")
+ C()
+ val d = remember { rememberedObject("d") }
+ report("C(${d.name})")
+ D()
+ }
+ }
+ }
+ }
+
+ awaiter.await()
+ }
+ // Same Legend as canRecordAComposition except the addition of the C(N) added above and
+ // +a, +b, etc. which records when the remembered object are sent the on-remember. This
+ // ensures that all onRemember calls are made after the composition has completed.
+ assertEquals(
+ "C(a), +B, *Linear, -B, C(b), +B, *Linear, -B, C(c), C(d), +C, ^x, *Text, -C, +D, " +
+ "-D, +D:1, *C, *C, *C, -D:1, +C, ^x, *Text, -C, +C, ^x, *Text, -C, +C, ^x, *Text, " +
+ "-C, +a, +b, +c, +d",
+ recording
+ )
+ }
+
+ @Suppress("ListIterator")
+ @Test
+ fun pausable_testRemember_RememberForgetOrder() = compositionTest {
+ var order = 0
+ val objects = mutableListOf<Any>()
+ val newRememberObject = { name: String ->
+ object : RememberObserver, Counted, Ordered, Named {
+ override var name = name
+ override var count = 0
+ override var rememberOrder = -1
+ override var forgetOrder = -1
+
+ override fun onRemembered() {
+ assertEquals(-1, rememberOrder, "Only one call to onRemembered expected")
+ rememberOrder = order++
+ count++
+ }
+
+ override fun onForgotten() {
+ assertEquals(-1, forgetOrder, "Only one call to onForgotten expected")
+ forgetOrder = order++
+ count--
+ }
+
+ override fun onAbandoned() {
+ assertEquals(0, count, "onAbandoned called after onRemembered")
+ }
+ }
+ .also { objects.add(it) }
+ }
+
+ @Suppress("UNUSED_PARAMETER") fun used(v: Any) {}
+
+ @Composable
+ fun Tree() {
+ used(remember { newRememberObject("L0B") })
+ Linear {
+ used(remember { newRememberObject("L1B") })
+ Linear {
+ used(remember { newRememberObject("L2B") })
+ Linear {
+ used(remember { newRememberObject("L3B") })
+ Linear { used(remember { newRememberObject("Leaf") }) }
+ used(remember { newRememberObject("L3A") })
+ }
+ used(remember { newRememberObject("L2A") })
+ }
+ used(remember { newRememberObject("L1A") })
+ }
+ used(remember { newRememberObject("L0A") })
+ }
+
+ val awaiter = Awaiter()
+ val workFlow = normalWorkflow { awaiter.done() }
+
+ compose { PausableContent(workFlow) { Tree() } }
+ awaiter.await()
+
+ // Legend:
+ // L<N><B|A>: where N is the nesting level and B is before the children and
+ // A is after the children.
+ // Leaf: the object remembered in the middle.
+ // This is asserting that the remember order is the same as it would have been had the
+ // above composition was not paused.
+ assertEquals(
+ "L0B, L1B, L2B, L3B, Leaf, L3A, L2A, L1A, L0A",
+ objects
+ .mapNotNull { it as? Ordered }
+ .sortedBy { it.rememberOrder }
+ .joinToString { (it as Named).name },
+ "Expected enter order",
+ )
+ }
+}
+
+fun String.splitRecording() = split(", ")
+
+typealias Workflow = suspend PausableContentWorkflowScope.() -> Unit
+
+fun workflow(workflow: Workflow): Workflow = workflow
+
+fun reuseWorkflow(done: Workflow = {}) = workflow {
+ setContentWithReuse()
+ resumeTillComplete { true }
+ apply()
+ done()
+}
+
+fun normalWorkflow(done: Workflow = {}) = workflow {
+ setContent()
+ resumeTillComplete { true }
+ apply()
+ done()
+}
+
+private interface TestRecorder {
+ fun log(message: String)
+
+ fun logs(): String
+
+ fun clear()
+}
+
+private var recorder: TestRecorder =
+ object : TestRecorder {
+ override fun log(message: String) {}
+
+ override fun logs(): String = ""
+
+ override fun clear() {}
+ }
+
+private inline fun recordTest(block: () -> Unit): String {
+ val result = mutableListOf<String>()
+ val oldRecorder = recorder
+ recorder =
+ object : TestRecorder {
+ override fun log(message: String) {
+ result.add(message)
+ }
+
+ override fun logs() = result.joinToString()
+
+ override fun clear() {
+ result.clear()
+ }
+ }
+ block()
+ recorder = oldRecorder
+ return result.joinToString()
+}
+
+private fun report(message: String) {
+ synchronized(recorder) { recorder.log(message) }
+}
+
+private inline fun report(message: String, block: () -> Unit) {
+ report("+$message")
+ block()
+ report("-$message")
+}
+
+@Composable
+private fun A() {
+ report("A") {
+ report("^z")
+ val z = remember { 0 }
+ report("^Y")
+ val y = remember { 1 }
+ Text("A: $z $y")
+ report("*B")
+ B {
+ report("A:1") {
+ report("*C")
+ C()
+ report("*D")
+ D()
+ }
+ }
+ }
+}
+
+private fun MockViewValidator.PausableContent(content: MockViewValidator.() -> Unit) {
+ this.view("PausableContentHost") { this.view("PausableContent", content) }
+}
+
+private fun MockViewValidator.A() {
+ Text("A: 0 1")
+ this.B {
+ this.C()
+ this.D()
+ }
+}
+
+@Composable
+private fun B(content: @Composable () -> Unit) {
+ report("B") {
+ report("*Linear")
+ Linear(content)
+ }
+}
+
+private fun MockViewValidator.B(content: MockViewValidator.() -> Unit) {
+ this.Linear(content)
+}
+
+@Composable
+private fun C() {
+ report("C") {
+ report("^x")
+ val x = remember { 3 }
+ report("*Text")
+ Text("C: $x")
+ }
+}
+
+private fun MockViewValidator.C() {
+ this.Text("C: 3")
+}
+
+@Composable
+private fun D() {
+ report("D") {
+ Linear {
+ report("D:1") {
+ repeat(3) {
+ report("*C")
+ C()
+ }
+ }
+ }
+ }
+}
+
+private fun MockViewValidator.D() {
+ this.Linear { repeat(3) { this.C() } }
+}
+
+interface PausableContentWorkflowScope {
+ val iteration: Int
+ val applied: Boolean
+
+ fun setContent(): PausedComposition
+
+ fun setContentWithReuse(): PausedComposition
+
+ fun resumeTillComplete(shouldPause: () -> Boolean)
+
+ fun apply()
+}
+
+fun PausableContentWorkflowScope.run(shouldPause: () -> Boolean = { true }) {
+ setContent()
+ resumeTillComplete(shouldPause)
+ apply()
+}
+
+class PausableContentWorkflowDriver(
+ private val composition: PausableComposition,
+ private val content: @Composable () -> Unit,
+ private var host: View?,
+ private var contentView: View?
+) : PausableContentWorkflowScope {
+ private var pausedComposition: PausedComposition? = null
+ override var iteration = 0
+ override val applied: Boolean
+ get() = host == null && pausedComposition == null
+
+ override fun setContent(): PausedComposition {
+ checkPrecondition(pausedComposition == null)
+ return composition.setPausableContent(content).also { pausedComposition = it }
+ }
+
+ override fun setContentWithReuse(): PausedComposition {
+ checkPrecondition(pausedComposition == null)
+ return composition.setPausableContentWithReuse(content).also { pausedComposition = it }
+ }
+
+ override fun resumeTillComplete(shouldPause: () -> Boolean) {
+ val pausedComposition = pausedComposition
+ checkPrecondition(pausedComposition != null)
+ while (!pausedComposition.isComplete) {
+ pausedComposition.resume(shouldPause)
+ iteration++
+ }
+ }
+
+ override fun apply() {
+ val pausedComposition = pausedComposition
+ checkPrecondition(pausedComposition != null && pausedComposition.isComplete)
+ pausedComposition.apply()
+ this.pausedComposition = null
+ val host = host
+ val contentView = contentView
+ if (host != null && contentView != null) {
+ host.children.add(contentView)
+ this.host = null
+ this.contentView = null
+ }
+ }
+}
+
+@Composable
+private fun PausableContent(
+ workflow: suspend PausableContentWorkflowScope.() -> Unit = { run() },
+ createApplier: (view: View) -> Applier<View> = { ViewApplier(it) },
+ content: @Composable () -> Unit
+) {
+ val host = View().also { it.name = "PausableContentHost" }
+ val pausableContent = View().also { it.name = "PausableContent" }
+ ComposeNode<View, ViewApplier>(factory = { host }, update = {})
+ val parent = rememberCompositionContext()
+ val composition =
+ remember(parent) { PausableComposition(createApplier(pausableContent), parent) }
+ LaunchedEffect(content as Any) {
+ val scope = PausableContentWorkflowDriver(composition, content, host, pausableContent)
+ scope.workflow()
+ }
+ DisposableEffect(Unit) { onDispose { composition.dispose() } }
+}
+
+private class Awaiter {
+ private var continuation: CancellableContinuation<Unit>? = null
+ private var done = false
+
+ suspend fun await() {
+ if (!done) {
+ suspendCancellableCoroutine { continuation = it }
+ }
+ }
+
+ fun resume() {
+ val current = continuation
+ continuation = null
+ current?.resume(Unit)
+ }
+
+ fun done() {
+ done = true
+ resume()
+ }
+}
diff --git a/compose/test-utils/build.gradle b/compose/test-utils/build.gradle
index 1fe35c8..4e9e30d 100644
--- a/compose/test-utils/build.gradle
+++ b/compose/test-utils/build.gradle
@@ -38,10 +38,10 @@
commonMain {
dependencies {
implementation(libs.kotlinStdlibCommon)
- implementation(projectOrArtifact(":compose:runtime:runtime"))
- implementation(projectOrArtifact(":compose:ui:ui-unit"))
- implementation(projectOrArtifact(":compose:ui:ui-graphics"))
- implementation(projectOrArtifact(":compose:ui:ui-test-junit4"))
+ implementation(project(":compose:runtime:runtime"))
+ implementation(project(":compose:ui:ui-unit"))
+ implementation(project(":compose:ui:ui-graphics"))
+ implementation(project(":compose:ui:ui-test-junit4"))
}
}
androidMain.dependencies {
@@ -49,11 +49,11 @@
// workaround for https://github.com/gradle/gradle/issues/8489
implementation("androidx.lifecycle:lifecycle-common:2.6.1")
implementation "androidx.activity:activity-compose:1.3.1"
- api(projectOrArtifact(":compose:ui:ui-test-junit4"))
+ api(project(":compose:ui:ui-test-junit4"))
api(project(":test:screenshot:screenshot"))
// This has stub APIs for access to legacy Android APIs, so we don't want
// any dependency on this module.
- compileOnly(projectOrArtifact(":compose:ui:ui-android-stubs"))
+ compileOnly(project(":compose:ui:ui-android-stubs"))
implementation(libs.testCore)
implementation(libs.testRules)
}
@@ -86,7 +86,7 @@
dependsOn(commonTest)
dependencies {
implementation(libs.truth)
- implementation(projectOrArtifact(":compose:material:material"))
+ implementation(project(":compose:material:material"))
}
}
diff --git a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AccessibilityChecksTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AccessibilityChecksTest.kt
index e1b9966..30b464c 100644
--- a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AccessibilityChecksTest.kt
+++ b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AccessibilityChecksTest.kt
@@ -111,7 +111,7 @@
@Composable
private fun BoxWithMissingContentDescription() {
Box(
- Modifier.size(20.dp).semantics {
+ Modifier.size(48.dp).semantics {
// The SemanticsModifier will make this node importantForAccessibility
// Having no content description is now a violation
this.contentDescription = ""
diff --git a/compose/ui/ui-text/api/current.ignore b/compose/ui/ui-text/api/current.ignore
index dbd6e6b..4870663 100644
--- a/compose/ui/ui-text/api/current.ignore
+++ b/compose/ui/ui-text/api/current.ignore
@@ -1,33 +1,9 @@
// Baseline format: 1.0
-AddedMethod: androidx.compose.ui.text.Html_androidKt#fromHtml(androidx.compose.ui.text.AnnotatedString.Companion, String, androidx.compose.ui.text.TextLinkStyles, androidx.compose.ui.text.LinkInteractionListener):
- Added method androidx.compose.ui.text.Html_androidKt.fromHtml(androidx.compose.ui.text.AnnotatedString.Companion,String,androidx.compose.ui.text.TextLinkStyles,androidx.compose.ui.text.LinkInteractionListener)
-
-
-ChangedDeprecated: androidx.compose.ui.text.TextStyle#TextStyle(androidx.compose.ui.graphics.Brush, float, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.style.TextMotion):
- Constructor androidx.compose.ui.text.TextStyle has changed deprecation state true --> false
-ChangedDeprecated: androidx.compose.ui.text.TextStyle#TextStyle(long, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.style.TextMotion):
- Constructor androidx.compose.ui.text.TextStyle has changed deprecation state true --> false
-ChangedDeprecated: androidx.compose.ui.text.TextStyle#copy(androidx.compose.ui.graphics.Brush, float, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.style.TextMotion):
- Method androidx.compose.ui.text.TextStyle.copy has changed deprecation state true --> false
-ChangedDeprecated: androidx.compose.ui.text.TextStyle#copy(long, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.style.TextMotion):
- Method androidx.compose.ui.text.TextStyle.copy has changed deprecation state true --> false
-ChangedDeprecated: androidx.compose.ui.text.TextStyle#merge(long, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.TextMotion):
- Method androidx.compose.ui.text.TextStyle.merge has changed deprecation state true --> false
-
-
-RemovedMethod: androidx.compose.ui.text.Html_androidKt#fromHtml(androidx.compose.ui.text.AnnotatedString.Companion, String, androidx.compose.ui.text.LinkInteractionListener):
- Removed method androidx.compose.ui.text.Html_androidKt.fromHtml(androidx.compose.ui.text.AnnotatedString.Companion,String,androidx.compose.ui.text.LinkInteractionListener)
-RemovedMethod: androidx.compose.ui.text.TextLinkStyles#merge(androidx.compose.ui.text.TextLinkStyles):
- Removed method androidx.compose.ui.text.TextLinkStyles.merge(androidx.compose.ui.text.TextLinkStyles)
-RemovedMethod: androidx.compose.ui.text.TextStyle#TextStyle(androidx.compose.ui.graphics.Brush, float, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.style.TextMotion, androidx.compose.ui.text.TextLinkStyles):
- Removed constructor androidx.compose.ui.text.TextStyle(androidx.compose.ui.graphics.Brush,float,long,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontSynthesis,androidx.compose.ui.text.font.FontFamily,String,long,androidx.compose.ui.text.style.BaselineShift,androidx.compose.ui.text.style.TextGeometricTransform,androidx.compose.ui.text.intl.LocaleList,long,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.graphics.Shadow,androidx.compose.ui.graphics.drawscope.DrawStyle,int,int,long,androidx.compose.ui.text.style.TextIndent,androidx.compose.ui.text.PlatformTextStyle,androidx.compose.ui.text.style.LineHeightStyle,int,int,androidx.compose.ui.text.style.TextMotion,androidx.compose.ui.text.TextLinkStyles)
-RemovedMethod: androidx.compose.ui.text.TextStyle#TextStyle(long, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.style.TextMotion, androidx.compose.ui.text.TextLinkStyles):
- Removed constructor androidx.compose.ui.text.TextStyle(long,long,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontSynthesis,androidx.compose.ui.text.font.FontFamily,String,long,androidx.compose.ui.text.style.BaselineShift,androidx.compose.ui.text.style.TextGeometricTransform,androidx.compose.ui.text.intl.LocaleList,long,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.graphics.Shadow,androidx.compose.ui.graphics.drawscope.DrawStyle,int,int,long,androidx.compose.ui.text.style.TextIndent,androidx.compose.ui.text.PlatformTextStyle,androidx.compose.ui.text.style.LineHeightStyle,int,int,androidx.compose.ui.text.style.TextMotion,androidx.compose.ui.text.TextLinkStyles)
-RemovedMethod: androidx.compose.ui.text.TextStyle#copy(androidx.compose.ui.graphics.Brush, float, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.style.TextMotion, androidx.compose.ui.text.TextLinkStyles):
- Removed method androidx.compose.ui.text.TextStyle.copy(androidx.compose.ui.graphics.Brush,float,long,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontSynthesis,androidx.compose.ui.text.font.FontFamily,String,long,androidx.compose.ui.text.style.BaselineShift,androidx.compose.ui.text.style.TextGeometricTransform,androidx.compose.ui.text.intl.LocaleList,long,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.graphics.Shadow,androidx.compose.ui.graphics.drawscope.DrawStyle,int,int,long,androidx.compose.ui.text.style.TextIndent,androidx.compose.ui.text.PlatformTextStyle,androidx.compose.ui.text.style.LineHeightStyle,int,int,androidx.compose.ui.text.style.TextMotion,androidx.compose.ui.text.TextLinkStyles)
-RemovedMethod: androidx.compose.ui.text.TextStyle#copy(long, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.style.TextMotion, androidx.compose.ui.text.TextLinkStyles):
- Removed method androidx.compose.ui.text.TextStyle.copy(long,long,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontSynthesis,androidx.compose.ui.text.font.FontFamily,String,long,androidx.compose.ui.text.style.BaselineShift,androidx.compose.ui.text.style.TextGeometricTransform,androidx.compose.ui.text.intl.LocaleList,long,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.graphics.Shadow,androidx.compose.ui.graphics.drawscope.DrawStyle,int,int,long,androidx.compose.ui.text.style.TextIndent,androidx.compose.ui.text.PlatformTextStyle,androidx.compose.ui.text.style.LineHeightStyle,int,int,androidx.compose.ui.text.style.TextMotion,androidx.compose.ui.text.TextLinkStyles)
-RemovedMethod: androidx.compose.ui.text.TextStyle#getLinkStyles():
- Removed method androidx.compose.ui.text.TextStyle.getLinkStyles()
-RemovedMethod: androidx.compose.ui.text.TextStyle#merge(long, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.TextMotion, androidx.compose.ui.text.TextLinkStyles):
- Removed method androidx.compose.ui.text.TextStyle.merge(long,long,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontSynthesis,androidx.compose.ui.text.font.FontFamily,String,long,androidx.compose.ui.text.style.BaselineShift,androidx.compose.ui.text.style.TextGeometricTransform,androidx.compose.ui.text.intl.LocaleList,long,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.graphics.Shadow,androidx.compose.ui.graphics.drawscope.DrawStyle,int,int,long,androidx.compose.ui.text.style.TextIndent,androidx.compose.ui.text.style.LineHeightStyle,int,int,androidx.compose.ui.text.PlatformTextStyle,androidx.compose.ui.text.style.TextMotion,androidx.compose.ui.text.TextLinkStyles)
+DefaultValueChange: androidx.compose.ui.text.MultiParagraph#MultiParagraph(androidx.compose.ui.text.AnnotatedString, androidx.compose.ui.text.TextStyle, long, androidx.compose.ui.unit.Density, androidx.compose.ui.text.font.FontFamily.Resolver, java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>>, int, boolean) parameter #7:
+ Attempted to remove default value from parameter ellipsis in androidx.compose.ui.text.MultiParagraph
+DefaultValueChange: androidx.compose.ui.text.MultiParagraph#MultiParagraph(androidx.compose.ui.text.MultiParagraphIntrinsics, long, int, boolean) parameter #3:
+ Attempted to remove default value from parameter ellipsis in androidx.compose.ui.text.MultiParagraph
+DefaultValueChange: androidx.compose.ui.text.ParagraphKt#Paragraph(String, androidx.compose.ui.text.TextStyle, long, androidx.compose.ui.unit.Density, androidx.compose.ui.text.font.FontFamily.Resolver, java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.SpanStyle>>, java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>>, int, boolean) parameter #8:
+ Attempted to remove default value from parameter ellipsis in androidx.compose.ui.text.ParagraphKt.Paragraph
+DefaultValueChange: androidx.compose.ui.text.ParagraphKt#Paragraph(androidx.compose.ui.text.ParagraphIntrinsics, long, int, boolean) parameter #3:
+ Attempted to remove default value from parameter ellipsis in androidx.compose.ui.text.ParagraphKt.Paragraph
diff --git a/compose/ui/ui-text/api/current.txt b/compose/ui/ui-text/api/current.txt
index 7ccf854..46a0d57 100644
--- a/compose/ui/ui-text/api/current.txt
+++ b/compose/ui/ui-text/api/current.txt
@@ -167,9 +167,11 @@
public final class MultiParagraph {
ctor @Deprecated public MultiParagraph(androidx.compose.ui.text.AnnotatedString annotatedString, androidx.compose.ui.text.TextStyle style, float width, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional boolean ellipsis);
ctor @Deprecated public MultiParagraph(androidx.compose.ui.text.AnnotatedString annotatedString, androidx.compose.ui.text.TextStyle style, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional boolean ellipsis, float width, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.Font.ResourceLoader resourceLoader);
- ctor public MultiParagraph(androidx.compose.ui.text.AnnotatedString annotatedString, androidx.compose.ui.text.TextStyle style, long constraints, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional boolean ellipsis);
+ ctor @Deprecated public MultiParagraph(androidx.compose.ui.text.AnnotatedString annotatedString, androidx.compose.ui.text.TextStyle style, long constraints, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, boolean ellipsis);
+ ctor public MultiParagraph(androidx.compose.ui.text.AnnotatedString annotatedString, androidx.compose.ui.text.TextStyle style, long constraints, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional int overflow);
ctor @Deprecated public MultiParagraph(androidx.compose.ui.text.MultiParagraphIntrinsics intrinsics, optional int maxLines, optional boolean ellipsis, float width);
- ctor public MultiParagraph(androidx.compose.ui.text.MultiParagraphIntrinsics intrinsics, long constraints, optional int maxLines, optional boolean ellipsis);
+ ctor @Deprecated public MultiParagraph(androidx.compose.ui.text.MultiParagraphIntrinsics intrinsics, long constraints, optional int maxLines, boolean ellipsis);
+ ctor public MultiParagraph(androidx.compose.ui.text.MultiParagraphIntrinsics intrinsics, long constraints, optional int maxLines, optional int overflow);
method public float[] fillBoundingBoxes(long range, float[] array, @IntRange(from=0L) int arrayStart);
method public androidx.compose.ui.text.style.ResolvedTextDirection getBidiRunDirection(int offset);
method public androidx.compose.ui.geometry.Rect getBoundingBox(int offset);
@@ -295,10 +297,12 @@
public final class ParagraphKt {
method @Deprecated public static androidx.compose.ui.text.Paragraph Paragraph(androidx.compose.ui.text.ParagraphIntrinsics paragraphIntrinsics, optional int maxLines, optional boolean ellipsis, float width);
- method public static androidx.compose.ui.text.Paragraph Paragraph(androidx.compose.ui.text.ParagraphIntrinsics paragraphIntrinsics, long constraints, optional int maxLines, optional boolean ellipsis);
+ method @Deprecated public static androidx.compose.ui.text.Paragraph Paragraph(androidx.compose.ui.text.ParagraphIntrinsics paragraphIntrinsics, long constraints, optional int maxLines, boolean ellipsis);
+ method public static androidx.compose.ui.text.Paragraph Paragraph(androidx.compose.ui.text.ParagraphIntrinsics paragraphIntrinsics, long constraints, optional int maxLines, optional int overflow);
method @Deprecated public static androidx.compose.ui.text.Paragraph Paragraph(String text, androidx.compose.ui.text.TextStyle style, float width, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.SpanStyle>> spanStyles, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional boolean ellipsis);
method @Deprecated public static androidx.compose.ui.text.Paragraph Paragraph(String text, androidx.compose.ui.text.TextStyle style, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.SpanStyle>> spanStyles, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional boolean ellipsis, float width, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.Font.ResourceLoader resourceLoader);
- method public static androidx.compose.ui.text.Paragraph Paragraph(String text, androidx.compose.ui.text.TextStyle style, long constraints, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.SpanStyle>> spanStyles, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional boolean ellipsis);
+ method @Deprecated public static androidx.compose.ui.text.Paragraph Paragraph(String text, androidx.compose.ui.text.TextStyle style, long constraints, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.SpanStyle>> spanStyles, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, boolean ellipsis);
+ method public static androidx.compose.ui.text.Paragraph Paragraph(String text, androidx.compose.ui.text.TextStyle style, long constraints, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.SpanStyle>> spanStyles, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional int overflow);
}
@androidx.compose.runtime.Immutable public final class ParagraphStyle implements androidx.compose.ui.text.AnnotatedString.Annotation {
@@ -1693,9 +1697,13 @@
public static final class TextOverflow.Companion {
method public int getClip();
method public int getEllipsis();
+ method public int getMiddleEllipsis();
+ method public int getStartEllipsis();
method public int getVisible();
property public final int Clip;
property public final int Ellipsis;
+ property public final int MiddleEllipsis;
+ property public final int StartEllipsis;
property public final int Visible;
}
diff --git a/compose/ui/ui-text/api/restricted_current.ignore b/compose/ui/ui-text/api/restricted_current.ignore
index dbd6e6b..4870663 100644
--- a/compose/ui/ui-text/api/restricted_current.ignore
+++ b/compose/ui/ui-text/api/restricted_current.ignore
@@ -1,33 +1,9 @@
// Baseline format: 1.0
-AddedMethod: androidx.compose.ui.text.Html_androidKt#fromHtml(androidx.compose.ui.text.AnnotatedString.Companion, String, androidx.compose.ui.text.TextLinkStyles, androidx.compose.ui.text.LinkInteractionListener):
- Added method androidx.compose.ui.text.Html_androidKt.fromHtml(androidx.compose.ui.text.AnnotatedString.Companion,String,androidx.compose.ui.text.TextLinkStyles,androidx.compose.ui.text.LinkInteractionListener)
-
-
-ChangedDeprecated: androidx.compose.ui.text.TextStyle#TextStyle(androidx.compose.ui.graphics.Brush, float, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.style.TextMotion):
- Constructor androidx.compose.ui.text.TextStyle has changed deprecation state true --> false
-ChangedDeprecated: androidx.compose.ui.text.TextStyle#TextStyle(long, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.style.TextMotion):
- Constructor androidx.compose.ui.text.TextStyle has changed deprecation state true --> false
-ChangedDeprecated: androidx.compose.ui.text.TextStyle#copy(androidx.compose.ui.graphics.Brush, float, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.style.TextMotion):
- Method androidx.compose.ui.text.TextStyle.copy has changed deprecation state true --> false
-ChangedDeprecated: androidx.compose.ui.text.TextStyle#copy(long, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.style.TextMotion):
- Method androidx.compose.ui.text.TextStyle.copy has changed deprecation state true --> false
-ChangedDeprecated: androidx.compose.ui.text.TextStyle#merge(long, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.TextMotion):
- Method androidx.compose.ui.text.TextStyle.merge has changed deprecation state true --> false
-
-
-RemovedMethod: androidx.compose.ui.text.Html_androidKt#fromHtml(androidx.compose.ui.text.AnnotatedString.Companion, String, androidx.compose.ui.text.LinkInteractionListener):
- Removed method androidx.compose.ui.text.Html_androidKt.fromHtml(androidx.compose.ui.text.AnnotatedString.Companion,String,androidx.compose.ui.text.LinkInteractionListener)
-RemovedMethod: androidx.compose.ui.text.TextLinkStyles#merge(androidx.compose.ui.text.TextLinkStyles):
- Removed method androidx.compose.ui.text.TextLinkStyles.merge(androidx.compose.ui.text.TextLinkStyles)
-RemovedMethod: androidx.compose.ui.text.TextStyle#TextStyle(androidx.compose.ui.graphics.Brush, float, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.style.TextMotion, androidx.compose.ui.text.TextLinkStyles):
- Removed constructor androidx.compose.ui.text.TextStyle(androidx.compose.ui.graphics.Brush,float,long,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontSynthesis,androidx.compose.ui.text.font.FontFamily,String,long,androidx.compose.ui.text.style.BaselineShift,androidx.compose.ui.text.style.TextGeometricTransform,androidx.compose.ui.text.intl.LocaleList,long,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.graphics.Shadow,androidx.compose.ui.graphics.drawscope.DrawStyle,int,int,long,androidx.compose.ui.text.style.TextIndent,androidx.compose.ui.text.PlatformTextStyle,androidx.compose.ui.text.style.LineHeightStyle,int,int,androidx.compose.ui.text.style.TextMotion,androidx.compose.ui.text.TextLinkStyles)
-RemovedMethod: androidx.compose.ui.text.TextStyle#TextStyle(long, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.style.TextMotion, androidx.compose.ui.text.TextLinkStyles):
- Removed constructor androidx.compose.ui.text.TextStyle(long,long,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontSynthesis,androidx.compose.ui.text.font.FontFamily,String,long,androidx.compose.ui.text.style.BaselineShift,androidx.compose.ui.text.style.TextGeometricTransform,androidx.compose.ui.text.intl.LocaleList,long,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.graphics.Shadow,androidx.compose.ui.graphics.drawscope.DrawStyle,int,int,long,androidx.compose.ui.text.style.TextIndent,androidx.compose.ui.text.PlatformTextStyle,androidx.compose.ui.text.style.LineHeightStyle,int,int,androidx.compose.ui.text.style.TextMotion,androidx.compose.ui.text.TextLinkStyles)
-RemovedMethod: androidx.compose.ui.text.TextStyle#copy(androidx.compose.ui.graphics.Brush, float, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.style.TextMotion, androidx.compose.ui.text.TextLinkStyles):
- Removed method androidx.compose.ui.text.TextStyle.copy(androidx.compose.ui.graphics.Brush,float,long,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontSynthesis,androidx.compose.ui.text.font.FontFamily,String,long,androidx.compose.ui.text.style.BaselineShift,androidx.compose.ui.text.style.TextGeometricTransform,androidx.compose.ui.text.intl.LocaleList,long,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.graphics.Shadow,androidx.compose.ui.graphics.drawscope.DrawStyle,int,int,long,androidx.compose.ui.text.style.TextIndent,androidx.compose.ui.text.PlatformTextStyle,androidx.compose.ui.text.style.LineHeightStyle,int,int,androidx.compose.ui.text.style.TextMotion,androidx.compose.ui.text.TextLinkStyles)
-RemovedMethod: androidx.compose.ui.text.TextStyle#copy(long, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.style.TextMotion, androidx.compose.ui.text.TextLinkStyles):
- Removed method androidx.compose.ui.text.TextStyle.copy(long,long,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontSynthesis,androidx.compose.ui.text.font.FontFamily,String,long,androidx.compose.ui.text.style.BaselineShift,androidx.compose.ui.text.style.TextGeometricTransform,androidx.compose.ui.text.intl.LocaleList,long,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.graphics.Shadow,androidx.compose.ui.graphics.drawscope.DrawStyle,int,int,long,androidx.compose.ui.text.style.TextIndent,androidx.compose.ui.text.PlatformTextStyle,androidx.compose.ui.text.style.LineHeightStyle,int,int,androidx.compose.ui.text.style.TextMotion,androidx.compose.ui.text.TextLinkStyles)
-RemovedMethod: androidx.compose.ui.text.TextStyle#getLinkStyles():
- Removed method androidx.compose.ui.text.TextStyle.getLinkStyles()
-RemovedMethod: androidx.compose.ui.text.TextStyle#merge(long, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.TextMotion, androidx.compose.ui.text.TextLinkStyles):
- Removed method androidx.compose.ui.text.TextStyle.merge(long,long,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontSynthesis,androidx.compose.ui.text.font.FontFamily,String,long,androidx.compose.ui.text.style.BaselineShift,androidx.compose.ui.text.style.TextGeometricTransform,androidx.compose.ui.text.intl.LocaleList,long,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.graphics.Shadow,androidx.compose.ui.graphics.drawscope.DrawStyle,int,int,long,androidx.compose.ui.text.style.TextIndent,androidx.compose.ui.text.style.LineHeightStyle,int,int,androidx.compose.ui.text.PlatformTextStyle,androidx.compose.ui.text.style.TextMotion,androidx.compose.ui.text.TextLinkStyles)
+DefaultValueChange: androidx.compose.ui.text.MultiParagraph#MultiParagraph(androidx.compose.ui.text.AnnotatedString, androidx.compose.ui.text.TextStyle, long, androidx.compose.ui.unit.Density, androidx.compose.ui.text.font.FontFamily.Resolver, java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>>, int, boolean) parameter #7:
+ Attempted to remove default value from parameter ellipsis in androidx.compose.ui.text.MultiParagraph
+DefaultValueChange: androidx.compose.ui.text.MultiParagraph#MultiParagraph(androidx.compose.ui.text.MultiParagraphIntrinsics, long, int, boolean) parameter #3:
+ Attempted to remove default value from parameter ellipsis in androidx.compose.ui.text.MultiParagraph
+DefaultValueChange: androidx.compose.ui.text.ParagraphKt#Paragraph(String, androidx.compose.ui.text.TextStyle, long, androidx.compose.ui.unit.Density, androidx.compose.ui.text.font.FontFamily.Resolver, java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.SpanStyle>>, java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>>, int, boolean) parameter #8:
+ Attempted to remove default value from parameter ellipsis in androidx.compose.ui.text.ParagraphKt.Paragraph
+DefaultValueChange: androidx.compose.ui.text.ParagraphKt#Paragraph(androidx.compose.ui.text.ParagraphIntrinsics, long, int, boolean) parameter #3:
+ Attempted to remove default value from parameter ellipsis in androidx.compose.ui.text.ParagraphKt.Paragraph
diff --git a/compose/ui/ui-text/api/restricted_current.txt b/compose/ui/ui-text/api/restricted_current.txt
index 89caf66..ea1b9e1 100644
--- a/compose/ui/ui-text/api/restricted_current.txt
+++ b/compose/ui/ui-text/api/restricted_current.txt
@@ -167,9 +167,11 @@
public final class MultiParagraph {
ctor @Deprecated public MultiParagraph(androidx.compose.ui.text.AnnotatedString annotatedString, androidx.compose.ui.text.TextStyle style, float width, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional boolean ellipsis);
ctor @Deprecated public MultiParagraph(androidx.compose.ui.text.AnnotatedString annotatedString, androidx.compose.ui.text.TextStyle style, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional boolean ellipsis, float width, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.Font.ResourceLoader resourceLoader);
- ctor public MultiParagraph(androidx.compose.ui.text.AnnotatedString annotatedString, androidx.compose.ui.text.TextStyle style, long constraints, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional boolean ellipsis);
+ ctor @Deprecated public MultiParagraph(androidx.compose.ui.text.AnnotatedString annotatedString, androidx.compose.ui.text.TextStyle style, long constraints, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, boolean ellipsis);
+ ctor public MultiParagraph(androidx.compose.ui.text.AnnotatedString annotatedString, androidx.compose.ui.text.TextStyle style, long constraints, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional int overflow);
ctor @Deprecated public MultiParagraph(androidx.compose.ui.text.MultiParagraphIntrinsics intrinsics, optional int maxLines, optional boolean ellipsis, float width);
- ctor public MultiParagraph(androidx.compose.ui.text.MultiParagraphIntrinsics intrinsics, long constraints, optional int maxLines, optional boolean ellipsis);
+ ctor @Deprecated public MultiParagraph(androidx.compose.ui.text.MultiParagraphIntrinsics intrinsics, long constraints, optional int maxLines, boolean ellipsis);
+ ctor public MultiParagraph(androidx.compose.ui.text.MultiParagraphIntrinsics intrinsics, long constraints, optional int maxLines, optional int overflow);
method public float[] fillBoundingBoxes(long range, float[] array, @IntRange(from=0L) int arrayStart);
method public androidx.compose.ui.text.style.ResolvedTextDirection getBidiRunDirection(int offset);
method public androidx.compose.ui.geometry.Rect getBoundingBox(int offset);
@@ -295,10 +297,12 @@
public final class ParagraphKt {
method @Deprecated public static androidx.compose.ui.text.Paragraph Paragraph(androidx.compose.ui.text.ParagraphIntrinsics paragraphIntrinsics, optional int maxLines, optional boolean ellipsis, float width);
- method public static androidx.compose.ui.text.Paragraph Paragraph(androidx.compose.ui.text.ParagraphIntrinsics paragraphIntrinsics, long constraints, optional int maxLines, optional boolean ellipsis);
+ method @Deprecated public static androidx.compose.ui.text.Paragraph Paragraph(androidx.compose.ui.text.ParagraphIntrinsics paragraphIntrinsics, long constraints, optional int maxLines, boolean ellipsis);
+ method public static androidx.compose.ui.text.Paragraph Paragraph(androidx.compose.ui.text.ParagraphIntrinsics paragraphIntrinsics, long constraints, optional int maxLines, optional int overflow);
method @Deprecated public static androidx.compose.ui.text.Paragraph Paragraph(String text, androidx.compose.ui.text.TextStyle style, float width, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.SpanStyle>> spanStyles, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional boolean ellipsis);
method @Deprecated public static androidx.compose.ui.text.Paragraph Paragraph(String text, androidx.compose.ui.text.TextStyle style, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.SpanStyle>> spanStyles, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional boolean ellipsis, float width, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.Font.ResourceLoader resourceLoader);
- method public static androidx.compose.ui.text.Paragraph Paragraph(String text, androidx.compose.ui.text.TextStyle style, long constraints, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.SpanStyle>> spanStyles, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional boolean ellipsis);
+ method @Deprecated public static androidx.compose.ui.text.Paragraph Paragraph(String text, androidx.compose.ui.text.TextStyle style, long constraints, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.SpanStyle>> spanStyles, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, boolean ellipsis);
+ method public static androidx.compose.ui.text.Paragraph Paragraph(String text, androidx.compose.ui.text.TextStyle style, long constraints, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.SpanStyle>> spanStyles, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional int overflow);
}
@androidx.compose.runtime.Immutable public final class ParagraphStyle implements androidx.compose.ui.text.AnnotatedString.Annotation {
@@ -1704,9 +1708,13 @@
public static final class TextOverflow.Companion {
method public int getClip();
method public int getEllipsis();
+ method public int getMiddleEllipsis();
+ method public int getStartEllipsis();
method public int getVisible();
property public final int Clip;
property public final int Ellipsis;
+ property public final int MiddleEllipsis;
+ property public final int StartEllipsis;
property public final int Visible;
}
diff --git a/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/NonLinearFontScalingBenchmark.kt b/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/NonLinearFontScalingBenchmark.kt
index 967b8cc..1a54763 100644
--- a/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/NonLinearFontScalingBenchmark.kt
+++ b/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/NonLinearFontScalingBenchmark.kt
@@ -26,6 +26,7 @@
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.createFontFamilyResolver
import androidx.compose.ui.text.style.LineHeightStyle
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.em
@@ -93,7 +94,8 @@
private fun paragraph(text: String, width: Float, density: Density): Paragraph {
return Paragraph(
paragraphIntrinsics = paragraphIntrinsics(text, density),
- constraints = Constraints(maxWidth = ceil(width).toInt())
+ constraints = Constraints(maxWidth = ceil(width).toInt()),
+ overflow = TextOverflow.Clip
)
}
diff --git a/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/ParagraphBenchmark.kt b/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/ParagraphBenchmark.kt
index b0fbaea..8251b68 100644
--- a/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/ParagraphBenchmark.kt
+++ b/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/ParagraphBenchmark.kt
@@ -28,6 +28,7 @@
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.createFontFamilyResolver
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.sp
@@ -97,7 +98,8 @@
): Paragraph {
return Paragraph(
paragraphIntrinsics = paragraphIntrinsics(text, spanStyles),
- constraints = Constraints(maxWidth = ceil(width).toInt())
+ constraints = Constraints(maxWidth = ceil(width).toInt()),
+ overflow = TextOverflow.Clip
)
}
diff --git a/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/ParagraphMethodBenchmark.kt b/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/ParagraphMethodBenchmark.kt
index e06b9f6..7834ca4 100644
--- a/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/ParagraphMethodBenchmark.kt
+++ b/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/ParagraphMethodBenchmark.kt
@@ -24,6 +24,7 @@
import androidx.compose.ui.text.ParagraphIntrinsics
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.createFontFamilyResolver
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.sp
@@ -87,7 +88,8 @@
Constraints(
maxWidth =
ceil(paragraphIntrinsics.maxIntrinsicWidth / preferredLineCount).toInt()
- )
+ ),
+ overflow = TextOverflow.Clip
)
}
diff --git a/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/ParagraphWithLineHeightBenchmark.kt b/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/ParagraphWithLineHeightBenchmark.kt
index d480613..a5fd8ab 100644
--- a/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/ParagraphWithLineHeightBenchmark.kt
+++ b/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/ParagraphWithLineHeightBenchmark.kt
@@ -26,6 +26,7 @@
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.createFontFamilyResolver
import androidx.compose.ui.text.style.LineHeightStyle
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.sp
@@ -86,7 +87,8 @@
private fun paragraph(text: String, width: Float): Paragraph {
return Paragraph(
paragraphIntrinsics = paragraphIntrinsics(text),
- constraints = Constraints(maxWidth = ceil(width).toInt())
+ constraints = Constraints(maxWidth = ceil(width).toInt()),
+ overflow = TextOverflow.Clip
)
}
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt
index 83bbc1b..074a435 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt
@@ -18,6 +18,7 @@
import android.graphics.Paint
import android.graphics.Typeface
+import android.os.Build
import android.text.TextPaint
import android.text.style.AbsoluteSizeSpan
import android.text.style.BackgroundColorSpan
@@ -68,6 +69,7 @@
import androidx.compose.ui.text.style.TextGeometricTransform
import androidx.compose.ui.text.style.TextIndent
import androidx.compose.ui.text.style.TextMotion
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.em
@@ -1119,7 +1121,7 @@
simpleParagraph(
text = text,
style = TextStyle(fontFamily = basicFontFamily, fontSize = fontSize),
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
width = paragraphWidth
)
@@ -1139,7 +1141,7 @@
val paragraph =
simpleParagraph(
text = text,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = TextStyle(fontFamily = basicFontFamily, fontSize = fontSize),
width = paragraphWidth
@@ -1159,7 +1161,7 @@
val paragraph =
simpleParagraph(
text = text,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
maxLines = maxLines,
style = TextStyle(fontFamily = basicFontFamily, fontSize = fontSize),
width = paragraphWidth
@@ -1179,7 +1181,7 @@
val paragraph =
simpleParagraph(
text = text,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
style = TextStyle(fontFamily = basicFontFamily, fontSize = fontSize),
width = 4 * fontSize.toPx(),
height = 6 * fontSize.toPx(),
@@ -1199,7 +1201,7 @@
val paragraph =
simpleParagraph(
text = text,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
style = TextStyle(fontFamily = basicFontFamily, fontSize = fontSize),
width = 4 * fontSize.toPx(),
height = 2.2f * fontSize.toPx(), // fits 2 lines
@@ -1211,14 +1213,14 @@
}
@Test
- fun testEllipsis_withLimitedHeight_ellipsisFalse_doesNotEllipsis() {
+ fun testEllipsis_withLimitedHeight_overflowNotEllipsis_doesNotEllipsis() {
with(defaultDensity) {
val text = "This is a text"
val fontSize = 30.sp
val paragraph =
simpleParagraph(
text = text,
- ellipsis = false,
+ overflow = TextOverflow.Clip,
style = TextStyle(fontFamily = basicFontFamily, fontSize = fontSize),
width = 4 * fontSize.toPx(),
height = 2.2f * fontSize.toPx(), // fits 2 lines
@@ -1238,7 +1240,7 @@
val paragraph =
simpleParagraph(
text = text,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
style = TextStyle(fontFamily = basicFontFamily, fontSize = fontSize),
width = 4 * fontSize.toPx(),
height = 2.2f * fontSize.toPx(), // fits 2 lines
@@ -1258,7 +1260,7 @@
val paragraph =
simpleParagraph(
text = text,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
style = TextStyle(fontFamily = basicFontFamily, fontSize = fontSize),
width = 4 * fontSize.toPx(),
height = 4 * fontSize.toPx(),
@@ -1278,7 +1280,7 @@
val paragraph =
simpleParagraph(
text = text,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
style = TextStyle(fontFamily = basicFontFamily, fontSize = fontSize),
width = 4 * fontSize.toPx(),
height = fontSize.toPx() / 4
@@ -1300,7 +1302,7 @@
text = text,
spanStyles =
listOf(AnnotatedString.Range(SpanStyle(fontSize = fontSize * 2), 0, 2)),
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
style = TextStyle(fontFamily = basicFontFamily, fontSize = fontSize),
width = 4 * fontSize.toPx(),
height = 2.2f * fontSize.toPx() // fits 2 lines
@@ -1311,6 +1313,52 @@
}
}
+ // Experimentally verified that middle and start ellipsis don't work correctly on API 21
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP_MR1)
+ @Test
+ fun testEllipsis_withMaxLinesOne_doesStartEllipsis() {
+ with(defaultDensity) {
+ val text = "abcde"
+ val fontSize = 100.sp
+ val paragraphWidth = (text.length - 2f) * fontSize.toPx()
+ val paragraph =
+ simpleParagraph(
+ text = text,
+ overflow = TextOverflow.StartEllipsis,
+ maxLines = 1,
+ style = TextStyle(fontFamily = basicFontFamily, fontSize = fontSize),
+ width = paragraphWidth
+ )
+
+ assertThat(paragraph.isLineEllipsized(0)).isTrue()
+ assertThat(paragraph.getLineEllipsisOffset(0)).isEqualTo(0)
+ assertThat(paragraph.getLineEllipsisCount(0)).isEqualTo(3)
+ }
+ }
+
+ // Experimentally verified that middle and start ellipsis don't work correctly on API 21
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP_MR1)
+ @Test
+ fun testEllipsis_withMaxLinesOne_doesMiddleEllipsis() {
+ with(defaultDensity) {
+ val text = "abcde"
+ val fontSize = 100.sp
+ val paragraphWidth = (text.length - 2f) * fontSize.toPx()
+ val paragraph =
+ simpleParagraph(
+ text = text,
+ overflow = TextOverflow.MiddleEllipsis,
+ maxLines = 1,
+ style = TextStyle(fontFamily = basicFontFamily, fontSize = fontSize),
+ width = paragraphWidth
+ )
+
+ assertThat(paragraph.isLineEllipsized(0)).isTrue()
+ assertThat(paragraph.getLineEllipsisOffset(0)).isEqualTo(1)
+ assertThat(paragraph.getLineEllipsisCount(0)).isEqualTo(3)
+ }
+ }
+
@Test
fun testSpanStyle_fontSize_appliedOnTextPaint() {
with(defaultDensity) {
@@ -1909,7 +1957,7 @@
spanStyles = listOf(),
placeholders = listOf(),
maxLines = Int.MAX_VALUE,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
constraints = minWidthConstraints,
fontFamilyResolver = UncachedFontFamilyResolver(context),
density = defaultDensity,
@@ -1925,7 +1973,7 @@
spanStyles = listOf(),
placeholders = listOf(),
maxLines = Int.MAX_VALUE,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
constraints = minHeightConstraints,
fontFamilyResolver = UncachedFontFamilyResolver(context),
density = defaultDensity,
@@ -2035,7 +2083,7 @@
spanStyles: List<AnnotatedString.Range<SpanStyle>> = listOf(),
textIndent: TextIndent? = null,
textAlign: TextAlign = TextAlign.Unspecified,
- ellipsis: Boolean = false,
+ overflow: TextOverflow = TextOverflow.Clip,
maxLines: Int = Int.MAX_VALUE,
width: Float,
height: Float = Float.POSITIVE_INFINITY,
@@ -2048,7 +2096,7 @@
placeholders = listOf(),
style = TextStyle(textAlign = textAlign, textIndent = textIndent).merge(style),
maxLines = maxLines,
- ellipsis = ellipsis,
+ overflow = overflow,
constraints = Constraints(maxWidth = width.ceilToInt(), maxHeight = height.ceilToInt()),
density = Density(density = 1f),
fontFamilyResolver = fontFamilyResolver
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphFillBoundingBoxesTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphFillBoundingBoxesTest.kt
index 39311f4..6e206d0 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphFillBoundingBoxesTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphFillBoundingBoxesTest.kt
@@ -20,6 +20,7 @@
import androidx.compose.ui.text.font.createFontFamilyResolver
import androidx.compose.ui.text.font.toFontFamily
import androidx.compose.ui.text.matchers.assertThat
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.sp
@@ -187,7 +188,8 @@
style = TextStyle(fontFamily = fontFamilyMeasureFont, fontSize = fontSize),
constraints = Constraints(maxWidth = width.ceilToInt()),
density = defaultDensity,
- fontFamilyResolver = fontFamilyResolver
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = TextOverflow.Clip
)
}
}
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphGetRangeForRectTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphGetRangeForRectTest.kt
index 6b6835d..9881518 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphGetRangeForRectTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphGetRangeForRectTest.kt
@@ -20,6 +20,7 @@
import androidx.compose.ui.text.font.toFontFamily
import androidx.compose.ui.text.intl.LocaleList
import androidx.compose.ui.text.matchers.assertThat
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.TextUnit
@@ -220,7 +221,8 @@
maxLines = maxLines,
constraints = Constraints(maxWidth = width.ceilToInt()),
density = defaultDensity,
- fontFamilyResolver = UncachedFontFamilyResolver(context)
+ fontFamilyResolver = UncachedFontFamilyResolver(context),
+ overflow = TextOverflow.Clip
)
}
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTest.kt
index 1490cd4..792816d 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTest.kt
@@ -34,6 +34,7 @@
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.text.style.TextIndent
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.TextUnit
@@ -1416,7 +1417,8 @@
style = TextStyle(textDirection = TextDirection.Rtl),
constraints = Constraints(),
density = defaultDensity,
- fontFamilyResolver = UncachedFontFamilyResolver(context)
+ fontFamilyResolver = UncachedFontFamilyResolver(context),
+ overflow = TextOverflow.Clip
)
// the first character uses TextDirection.Content, text is Ltr
@@ -1441,7 +1443,8 @@
placeholders = placeholders,
constraints = Constraints(),
density = defaultDensity,
- fontFamilyResolver = UncachedFontFamilyResolver(context)
+ fontFamilyResolver = UncachedFontFamilyResolver(context),
+ overflow = TextOverflow.Clip
)
// Rendered as below:
@@ -1465,7 +1468,8 @@
placeholders = placeholders,
constraints = Constraints(),
density = defaultDensity,
- fontFamilyResolver = UncachedFontFamilyResolver(context)
+ fontFamilyResolver = UncachedFontFamilyResolver(context),
+ overflow = TextOverflow.Clip
)
// Rendered as below:
@@ -1490,7 +1494,8 @@
placeholders = placeholders,
constraints = Constraints(),
density = defaultDensity,
- fontFamilyResolver = UncachedFontFamilyResolver(context)
+ fontFamilyResolver = UncachedFontFamilyResolver(context),
+ overflow = TextOverflow.Clip
)
assertThat(paragraph.placeholderRects).hasSize(1)
@@ -1524,7 +1529,8 @@
placeholders = placeholders,
constraints = Constraints(),
density = defaultDensity,
- fontFamilyResolver = UncachedFontFamilyResolver(context)
+ fontFamilyResolver = UncachedFontFamilyResolver(context),
+ overflow = TextOverflow.Clip
)
assertThat(paragraph.placeholderRects).hasSize(2)
@@ -1563,7 +1569,8 @@
placeholders = placeholders,
constraints = Constraints(),
density = defaultDensity,
- fontFamilyResolver = UncachedFontFamilyResolver(context)
+ fontFamilyResolver = UncachedFontFamilyResolver(context),
+ overflow = TextOverflow.Clip
)
}
@@ -1575,7 +1582,8 @@
style = TextStyle(),
constraints = minWidthConstraints,
density = defaultDensity,
- fontFamilyResolver = UncachedFontFamilyResolver(context)
+ fontFamilyResolver = UncachedFontFamilyResolver(context),
+ overflow = TextOverflow.Clip
)
}
@@ -1587,7 +1595,8 @@
style = TextStyle(),
constraints = minHeightConstraints,
density = defaultDensity,
- fontFamilyResolver = UncachedFontFamilyResolver(context)
+ fontFamilyResolver = UncachedFontFamilyResolver(context),
+ overflow = TextOverflow.Clip
)
}
@@ -1610,7 +1619,8 @@
style = TextStyle(fontSize = fontSize, fontFamily = fontFamilyMeasureFont),
constraints = constraints,
density = this,
- fontFamilyResolver = UncachedFontFamilyResolver(context)
+ fontFamilyResolver = UncachedFontFamilyResolver(context),
+ overflow = TextOverflow.Clip
)
}
}
@@ -1821,7 +1831,8 @@
maxLines = maxLines,
constraints = Constraints(maxWidth = width.ceilToInt()),
density = defaultDensity,
- fontFamilyResolver = UncachedFontFamilyResolver(context)
+ fontFamilyResolver = UncachedFontFamilyResolver(context),
+ overflow = TextOverflow.Clip
)
}
@@ -1845,7 +1856,8 @@
maxLines = maxLines,
constraints = Constraints(maxWidth = width.ceilToInt()),
density = defaultDensity,
- fontFamilyResolver = UncachedFontFamilyResolver(context)
+ fontFamilyResolver = UncachedFontFamilyResolver(context),
+ overflow = TextOverflow.Clip
)
}
}
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTextDirectionTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTextDirectionTest.kt
index f7dc800..e8064cb 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTextDirectionTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTextDirectionTest.kt
@@ -20,6 +20,7 @@
import androidx.compose.ui.text.intl.LocaleList
import androidx.compose.ui.text.style.ResolvedTextDirection
import androidx.compose.ui.text.style.TextDirection
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.TextUnit
@@ -286,7 +287,8 @@
),
constraints = Constraints(maxWidth = width.ceilToInt()),
density = defaultDensity,
- fontFamilyResolver = UncachedFontFamilyResolver(context)
+ fontFamilyResolver = UncachedFontFamilyResolver(context),
+ overflow = TextOverflow.Clip
)
}
}
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphFillBoundingBoxesTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphFillBoundingBoxesTest.kt
index 341c972..ca8432a 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphFillBoundingBoxesTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphFillBoundingBoxesTest.kt
@@ -28,6 +28,7 @@
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.text.style.TextGeometricTransform
import androidx.compose.ui.text.style.TextIndent
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.em
@@ -694,7 +695,7 @@
spanStyles = text.spanStyles,
placeholders = placeholders,
maxLines = Int.MAX_VALUE,
- ellipsis = false,
+ overflow = TextOverflow.Clip,
constraints = Constraints(maxWidth = width.ceilToInt()),
density = defaultDensity,
fontFamilyResolver = fontFamilyResolver
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationBoundingBoxTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationBoundingBoxTest.kt
index 7c2fa58..76b79a7 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationBoundingBoxTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationBoundingBoxTest.kt
@@ -18,6 +18,7 @@
import androidx.compose.ui.text.font.toFontFamily
import androidx.compose.ui.text.style.TextDirection
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.sp
@@ -358,7 +359,7 @@
text = TEST_CONTENT_MAP[textDirection]!![lineBreakFrom]!!,
style = TextStyle(fontFamily = fontFamilyMeasureFont, fontSize = fontSize),
maxLines = maxLines,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
constraints =
Constraints(
maxWidth = (widthInFontSize * fontSizeInPx),
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationIndentationFixTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationIndentationFixTest.kt
index e01f03d..a851fb5 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationIndentationFixTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationIndentationFixTest.kt
@@ -20,6 +20,7 @@
import androidx.compose.ui.text.font.toFontFamily
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextIndent
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.TextUnit
@@ -345,7 +346,7 @@
textIndent = textIndent
),
maxLines = lastLine + 1,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
constraints = Constraints(maxWidth = width),
density = Density(density = 1f),
fontFamilyResolver =
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationLineHeightStyleTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationLineHeightStyleTest.kt
index 07e2092..3d869ea 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationLineHeightStyleTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationLineHeightStyleTest.kt
@@ -22,6 +22,7 @@
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.text.style.LineHeightStyle.Alignment
import androidx.compose.ui.text.style.LineHeightStyle.Trim
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.sp
@@ -922,7 +923,6 @@
text: String = "",
style: TextStyle? = null,
maxLines: Int = Int.MAX_VALUE,
- ellipsis: Boolean = false,
spanStyles: List<AnnotatedString.Range<SpanStyle>> = listOf(),
width: Float = Float.MAX_VALUE
): Paragraph {
@@ -939,7 +939,7 @@
)
.merge(style),
maxLines = maxLines,
- ellipsis = ellipsis,
+ overflow = TextOverflow.Clip,
constraints = Constraints(maxWidth = width.ceilToInt()),
density = defaultDensity,
fontFamilyResolver = UncachedFontFamilyResolver(context)
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt
index 1d8aa21..e12b98e 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt
@@ -45,6 +45,7 @@
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.text.style.TextGeometricTransform
import androidx.compose.ui.text.style.TextIndent
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.em
@@ -759,7 +760,7 @@
text = text,
style = TextStyle(fontSize = fontSize),
width = 3 * fontSizeInPx,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
maxLines = 1
)
@@ -783,7 +784,7 @@
style = TextStyle(fontSize = fontSize),
width = 3 * fontSizeInPx,
height = fontSizeInPx,
- ellipsis = true
+ overflow = TextOverflow.Ellipsis
)
val box = paragraph.getBoundingBox(5)
@@ -806,7 +807,7 @@
style = TextStyle(fontSize = fontSize),
width = 3 * fontSizeInPx,
height = fontSizeInPx,
- ellipsis = true
+ overflow = TextOverflow.Ellipsis
)
val box = paragraph.getBoundingBox(4)
@@ -828,7 +829,7 @@
text = text,
style = TextStyle(fontSize = fontSize),
width = 3 * fontSizeInPx,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
maxLines = 1
)
@@ -851,7 +852,7 @@
text = text,
style = TextStyle(fontSize = fontSize),
width = 3 * fontSizeInPx,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
height = fontSizeInPx
)
@@ -874,7 +875,7 @@
text = text,
style = TextStyle(fontSize = fontSize),
width = 3 * fontSizeInPx,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
height = fontSizeInPx
)
@@ -2008,7 +2009,8 @@
density = defaultDensity,
fontFamilyResolver = resourceLoader,
// just have 10x font size to have a bitmap
- constraints = Constraints(maxWidth = (fontSizeInPx * 10).ceilToInt())
+ constraints = Constraints(maxWidth = (fontSizeInPx * 10).ceilToInt()),
+ overflow = TextOverflow.Clip
)
paragraph.bitmap()
@@ -2297,7 +2299,8 @@
fun didExceedMaxLines_ellipsis_withMaxLinesSmallerThanTextLines_returnsTrue() {
val text = "aaa\naa"
val maxLines = text.lines().size - 1
- val paragraph = simpleParagraph(text = text, maxLines = maxLines, ellipsis = true)
+ val paragraph =
+ simpleParagraph(text = text, maxLines = maxLines, overflow = TextOverflow.Ellipsis)
assertThat(paragraph.didExceedMaxLines).isTrue()
}
@@ -2306,7 +2309,8 @@
fun didExceedMaxLines_ellipsis_withMaxLinesEqualToTextLines_returnsFalse() {
val text = "aaa\naa"
val maxLines = text.lines().size
- val paragraph = simpleParagraph(text = text, maxLines = maxLines, ellipsis = true)
+ val paragraph =
+ simpleParagraph(text = text, maxLines = maxLines, overflow = TextOverflow.Ellipsis)
assertThat(paragraph.didExceedMaxLines).isFalse()
}
@@ -2315,7 +2319,8 @@
fun didExceedMaxLines_ellipsis_withMaxLinesGreaterThanTextLines_returnsFalse() {
val text = "aaa\naa"
val maxLines = text.lines().size + 1
- val paragraph = simpleParagraph(text = text, maxLines = maxLines, ellipsis = true)
+ val paragraph =
+ simpleParagraph(text = text, maxLines = maxLines, overflow = TextOverflow.Ellipsis)
assertThat(paragraph.didExceedMaxLines).isFalse()
}
@@ -2332,7 +2337,7 @@
text = text,
style = TextStyle(fontSize = fontSize),
maxLines = maxLines,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
// One line can only contain 1 character
width = fontSizeInPx
)
@@ -2345,7 +2350,8 @@
fun didExceedMaxLines_ellipsis_withMaxLinesEqualToTextLines_withLineWrap_returnsFalse() {
val text = "a"
val maxLines = text.lines().size
- val paragraph = simpleParagraph(text = text, maxLines = maxLines, ellipsis = true)
+ val paragraph =
+ simpleParagraph(text = text, maxLines = maxLines, overflow = TextOverflow.Ellipsis)
assertThat(paragraph.didExceedMaxLines).isFalse()
}
@@ -2362,7 +2368,7 @@
text = text,
style = TextStyle(fontSize = fontSize),
maxLines = maxLines,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
// One line can only contain 1 character
width = fontSizeInPx
)
@@ -2772,7 +2778,7 @@
simpleParagraph(
text = text,
maxLines = 1,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
style = TextStyle(),
width = Float.MAX_VALUE
)
@@ -2883,7 +2889,12 @@
val text = "aaa\nbbb\nccc"
val paragraph =
- simpleParagraph(text = text, maxLines = 2, ellipsis = true, width = Float.MAX_VALUE)
+ simpleParagraph(
+ text = text,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ width = Float.MAX_VALUE
+ )
assertThat(paragraph.lineCount).isEqualTo(2)
assertThat(paragraph.getLineEnd(0)).isEqualTo(4)
@@ -2903,7 +2914,7 @@
text = text,
style = TextStyle(fontFamily = fontFamilyMeasureFont, fontSize = 10.sp),
maxLines = 2,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
width = 50f
)
@@ -3028,7 +3039,7 @@
text = text,
style = textStyle,
maxLines = maxLines,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
width = 480f // px
)
as AndroidParagraph
@@ -4550,7 +4561,8 @@
val paragraph =
Paragraph(
paragraphIntrinsics = paragraphIntrinsics,
- constraints = Constraints(maxWidth = (fontSizeInPx * text.length).ceilToInt())
+ constraints = Constraints(maxWidth = (fontSizeInPx * text.length).ceilToInt()),
+ overflow = TextOverflow.Clip
)
assertThat(paragraph.maxIntrinsicWidth).isEqualTo(paragraphIntrinsics.maxIntrinsicWidth)
@@ -4807,7 +4819,7 @@
text: String = "",
style: TextStyle? = null,
maxLines: Int = Int.MAX_VALUE,
- ellipsis: Boolean = false,
+ overflow: TextOverflow = TextOverflow.Clip,
spanStyles: List<AnnotatedString.Range<SpanStyle>> = listOf(),
density: Density? = null,
width: Float = Float.MAX_VALUE,
@@ -4818,7 +4830,7 @@
spanStyles = spanStyles,
style = TextStyle(fontFamily = fontFamilyMeasureFont).merge(style),
maxLines = maxLines,
- ellipsis = ellipsis,
+ overflow = overflow,
constraints = Constraints(maxWidth = width.ceilToInt(), maxHeight = height.ceilToInt()),
density = density ?: defaultDensity,
fontFamilyResolver = UncachedFontFamilyResolver(context)
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTextDirectionTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTextDirectionTest.kt
index db2bd03..df5f5e6 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTextDirectionTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTextDirectionTest.kt
@@ -19,6 +19,7 @@
import androidx.compose.ui.text.intl.LocaleList
import androidx.compose.ui.text.style.ResolvedTextDirection
import androidx.compose.ui.text.style.TextDirection
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -64,7 +65,8 @@
style = TextStyle(textDirection = TextDirection.Unspecified),
constraints = Constraints(),
density = defaultDensity,
- fontFamilyResolver = resourceLoader
+ fontFamilyResolver = resourceLoader,
+ overflow = TextOverflow.Clip
)
assertThat(paragraph.getParagraphDirection(0)).isEqualTo(ResolvedTextDirection.Ltr)
@@ -80,7 +82,8 @@
style = TextStyle(textDirection = TextDirection.Unspecified),
constraints = Constraints(),
density = defaultDensity,
- fontFamilyResolver = resourceLoader
+ fontFamilyResolver = resourceLoader,
+ overflow = TextOverflow.Clip
)
assertThat(paragraph.getParagraphDirection(0)).isEqualTo(ResolvedTextDirection.Rtl)
@@ -98,7 +101,8 @@
),
constraints = Constraints(),
density = defaultDensity,
- fontFamilyResolver = resourceLoader
+ fontFamilyResolver = resourceLoader,
+ overflow = TextOverflow.Clip
)
assertThat(paragraph.getParagraphDirection(0)).isEqualTo(ResolvedTextDirection.Ltr)
@@ -116,7 +120,8 @@
),
constraints = Constraints(),
density = defaultDensity,
- fontFamilyResolver = resourceLoader
+ fontFamilyResolver = resourceLoader,
+ overflow = TextOverflow.Clip
)
assertThat(paragraph.getParagraphDirection(0)).isEqualTo(ResolvedTextDirection.Rtl)
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphPlaceholderIntegrationTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphPlaceholderIntegrationTest.kt
index 6fad0af..e475f7f 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphPlaceholderIntegrationTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphPlaceholderIntegrationTest.kt
@@ -18,6 +18,7 @@
import androidx.compose.ui.text.android.style.ceilToInt
import androidx.compose.ui.text.font.toFontFamily
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.TextUnit
@@ -504,7 +505,7 @@
fontSize = fontSize.sp,
width = 2 * fontSize,
maxLines = 1,
- ellipsis = true
+ overflow = TextOverflow.Ellipsis
)
val placeholderRects = paragraph.placeholderRects
assertThat(placeholderRects.size).isEqualTo(placeholders.size)
@@ -531,7 +532,7 @@
fontSize = fontSize.sp,
width = 2 * fontSize,
height = 1.3f * fontSize,
- ellipsis = true
+ overflow = TextOverflow.Ellipsis
)
val placeholderRects = paragraph.placeholderRects
assertThat(placeholderRects.size).isEqualTo(placeholders.size)
@@ -559,7 +560,6 @@
width = 2 * fontSize,
height = fontSize,
maxLines = 1,
- ellipsis = false
)
val placeholderRects = paragraph.placeholderRects
assertThat(placeholderRects.size).isEqualTo(placeholders.size)
@@ -576,7 +576,7 @@
width: Float = Float.MAX_VALUE,
height: Float = Float.MAX_VALUE,
maxLines: Int = Int.MAX_VALUE,
- ellipsis: Boolean = false
+ overflow: TextOverflow = TextOverflow.Clip
): Paragraph {
return Paragraph(
text = text,
@@ -584,7 +584,7 @@
spanStyles = spanStyles,
placeholders = placeholders,
maxLines = maxLines,
- ellipsis = ellipsis,
+ overflow = overflow,
constraints = Constraints(maxWidth = width.ceilToInt(), maxHeight = height.ceilToInt()),
density = defaultDensity,
fontFamilyResolver = UncachedFontFamilyResolver(context)
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt
index 180831d..17a6a87 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt
@@ -96,6 +96,36 @@
}
@Test
+ fun width_shouldMatter_ifSoftwrapIsDisabled_butOverflowIsStartEllipsis() {
+ val textLayoutResult =
+ layoutText(
+ textLayoutInput(
+ text = longText,
+ softWrap = false,
+ overflow = TextOverflow.StartEllipsis,
+ constraints = Constraints(maxWidth = 200)
+ )
+ )
+
+ assertThat(textLayoutResult.multiParagraph.width).isEqualTo(200)
+ }
+
+ @Test
+ fun width_shouldMatter_ifSoftwrapIsDisabled_butOverflowIsMiddleEllipsis() {
+ val textLayoutResult =
+ layoutText(
+ textLayoutInput(
+ text = longText,
+ softWrap = false,
+ overflow = TextOverflow.MiddleEllipsis,
+ constraints = Constraints(maxWidth = 200)
+ )
+ )
+
+ assertThat(textLayoutResult.multiParagraph.width).isEqualTo(200)
+ }
+
+ @Test
fun width_shouldBeMaxIntrinsicWidth_ifSoftwrapIsDisabled_andOverflowIsClip() {
val textLayoutResult =
layoutText(
@@ -144,7 +174,35 @@
}
@Test
- fun dontOverwriteMaxLines_ifSoftwrapIsEnabled() {
+ fun overwriteMaxLines_ifSoftwrapIsDisabled_andTextOverflowIsStartEllipsis() {
+ val textLayoutResult =
+ layoutText(
+ textLayoutInput(
+ text = multiLineText,
+ softWrap = false,
+ overflow = TextOverflow.StartEllipsis
+ )
+ )
+
+ assertThat(textLayoutResult.multiParagraph.lineCount).isEqualTo(1)
+ }
+
+ @Test
+ fun overwriteMaxLines_ifSoftwrapIsDisabled_andTextOverflowIsMiddleEllipsis() {
+ val textLayoutResult =
+ layoutText(
+ textLayoutInput(
+ text = multiLineText,
+ softWrap = false,
+ overflow = TextOverflow.MiddleEllipsis
+ )
+ )
+
+ assertThat(textLayoutResult.multiParagraph.lineCount).isEqualTo(1)
+ }
+
+ @Test
+ fun dontOverwriteMaxLines_endEllipsis_ifSoftwrapIsEnabled() {
val textLayoutResult =
layoutText(
textLayoutInput(
@@ -158,6 +216,34 @@
}
@Test
+ fun dontOverwriteMaxLines_middleEllipsis_ifSoftwrapIsEnabled() {
+ val textLayoutResult =
+ layoutText(
+ textLayoutInput(
+ text = multiLineText,
+ softWrap = true,
+ overflow = TextOverflow.MiddleEllipsis
+ )
+ )
+
+ assertThat(textLayoutResult.multiParagraph.lineCount).isEqualTo(5)
+ }
+
+ @Test
+ fun dontOverwriteMaxLines_startEllipsis_ifSoftwrapIsEnabled() {
+ val textLayoutResult =
+ layoutText(
+ textLayoutInput(
+ text = multiLineText,
+ softWrap = true,
+ overflow = TextOverflow.StartEllipsis
+ )
+ )
+
+ assertThat(textLayoutResult.multiParagraph.lineCount).isEqualTo(5)
+ }
+
+ @Test
fun disabledSoftwrap_andOverflowClip_shouldConstrainLayoutSize() {
val textLayoutResult =
layoutText(
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidParagraphGetRangeForRectTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidParagraphGetRangeForRectTest.kt
index 64d9fdb..c6b7358 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidParagraphGetRangeForRectTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidParagraphGetRangeForRectTest.kt
@@ -34,6 +34,7 @@
import androidx.compose.ui.text.rangeOf
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextIndent
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.sp
@@ -500,7 +501,6 @@
spanStyles: List<AnnotatedString.Range<SpanStyle>> = listOf(),
textIndent: TextIndent? = null,
textAlign: TextAlign = TextAlign.Unspecified,
- ellipsis: Boolean = false,
maxLines: Int = Int.MAX_VALUE,
width: Float,
height: Float = Float.POSITIVE_INFINITY,
@@ -519,7 +519,7 @@
)
.merge(style),
maxLines = maxLines,
- ellipsis = ellipsis,
+ overflow = TextOverflow.Clip,
constraints = Constraints(maxWidth = width.ceilToInt(), maxHeight = height.ceilToInt()),
density = Density(density = 1f),
fontFamilyResolver = fontFamilyResolver
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/style/TextLineBreaker.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/style/TextLineBreaker.kt
index 2187744..55b2fac 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/style/TextLineBreaker.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/style/TextLineBreaker.kt
@@ -43,7 +43,7 @@
placeholders = listOf(),
style = textStyle,
maxLines = Int.MAX_VALUE,
- ellipsis = false,
+ overflow = TextOverflow.Clip,
constraints =
Constraints(maxWidth = maxWidth, maxHeight = Float.POSITIVE_INFINITY.ceilToInt()),
density = density,
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidParagraph.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidParagraph.android.kt
index dfe0975..7425e87 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidParagraph.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidParagraph.android.kt
@@ -82,6 +82,10 @@
import androidx.compose.ui.text.style.ResolvedTextDirection
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.text.style.TextOverflow.Companion.Ellipsis
+import androidx.compose.ui.text.style.TextOverflow.Companion.MiddleEllipsis
+import androidx.compose.ui.text.style.TextOverflow.Companion.StartEllipsis
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.TextUnit
@@ -97,7 +101,7 @@
internal class AndroidParagraph(
val paragraphIntrinsics: AndroidParagraphIntrinsics,
val maxLines: Int,
- val ellipsis: Boolean,
+ val overflow: TextOverflow,
val constraints: Constraints
) : Paragraph {
constructor(
@@ -106,7 +110,7 @@
spanStyles: List<AnnotatedString.Range<SpanStyle>>,
placeholders: List<AnnotatedString.Range<Placeholder>>,
maxLines: Int,
- ellipsis: Boolean,
+ overflow: TextOverflow,
constraints: Constraints,
fontFamilyResolver: FontFamily.Resolver,
density: Density
@@ -121,7 +125,7 @@
density = density
),
maxLines = maxLines,
- ellipsis = ellipsis,
+ overflow = overflow,
constraints = constraints
)
@@ -139,7 +143,7 @@
val style = paragraphIntrinsics.style
charSequence =
- if (shouldAttachIndentationFixSpan(style, ellipsis)) {
+ if (shouldAttachIndentationFixSpan(style, overflow == Ellipsis)) {
// When letter spacing, align and ellipsize applied to text, the ellipsized line is
// indented wrong. This function adds the IndentationFixSpan in order to fix the
// issue
@@ -164,13 +168,14 @@
val lineBreakWordStyle = toLayoutLineBreakWordStyle(style.lineBreak.wordBreak)
val ellipsize =
- if (ellipsis) {
- TextUtils.TruncateAt.END
- } else {
- null
+ when (overflow) {
+ Ellipsis -> TextUtils.TruncateAt.END
+ MiddleEllipsis -> TextUtils.TruncateAt.MIDDLE
+ StartEllipsis -> TextUtils.TruncateAt.START
+ else -> null
}
- val firstLayout =
+ var firstLayout =
constructTextLayout(
alignment = alignment,
justificationMode = justificationMode,
@@ -182,8 +187,42 @@
lineBreakWordStyle = lineBreakWordStyle
)
- // Ellipsize if there's not enough vertical space to fit all lines
- if (ellipsis && firstLayout.height > constraints.maxHeight && maxLines > 1) {
+ // In case of start/middle ellipsis when the letter spacing is enabled and some of the
+ // characters are ellipsized away, we need to remeasure. This is because though
+ // internally ellipsized character are replaced with zero-width U+FEFF character, the
+ // letter spacing is still applied to each such character. It's been fixed on API 35
+ // where letter spacing won't be applied to some special characters including U+FEFF.
+ if (
+ Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM &&
+ textPaint.letterSpacing != 0f &&
+ (overflow == StartEllipsis || overflow == MiddleEllipsis) &&
+ firstLayout.getLineEllipsisCount(0) > 0
+ ) {
+ val beforeEllipsis = firstLayout.getLineEllipsisOffset(0)
+ val afterEllipsis = beforeEllipsis + firstLayout.getLineEllipsisCount(0)
+ val newSpannable =
+ TextUtils.concat(
+ charSequence.subSequence(0, beforeEllipsis),
+ Typography.ellipsis.toString(),
+ charSequence.subSequence(afterEllipsis, charSequence.length)
+ )
+ firstLayout =
+ constructTextLayout(
+ alignment = alignment,
+ justificationMode = justificationMode,
+ ellipsize = ellipsize,
+ maxLines = maxLines,
+ hyphens = hyphens,
+ breakStrategy = breakStrategy,
+ lineBreakStyle = lineBreakStyle,
+ lineBreakWordStyle = lineBreakWordStyle,
+ charSequence = newSpannable
+ )
+ }
+
+ // Ellipsize if there's not enough vertical space to fit all lines. Because this only makes
+ // sense for end ellipsis because start/middle only works for a single line.
+ if (overflow == Ellipsis && firstLayout.height > constraints.maxHeight && maxLines > 1) {
val calculatedMaxLines =
firstLayout.numberOfLinesThatFitMaxHeight(constraints.maxHeight)
layout =
@@ -194,10 +233,8 @@
ellipsize = ellipsize,
// When we can't fully fit even a single line, measure with one line anyway.
// This will allow to have an ellipsis on that single line. If we measured
- // with
- // 0 maxLines, it would measure all lines with no ellipsis even though the
- // first
- // line might be partially visible
+ // with 0 maxLines, it would measure all lines with no ellipsis even though
+ // the first line might be partially visible
maxLines = calculatedMaxLines.coerceAtLeast(1),
hyphens = hyphens,
breakStrategy = breakStrategy,
@@ -443,6 +480,11 @@
override fun isLineEllipsized(lineIndex: Int): Boolean = layout.isLineEllipsized(lineIndex)
+ internal fun getLineEllipsisOffset(lineIndex: Int): Int =
+ layout.getLineEllipsisOffset(lineIndex)
+
+ internal fun getLineEllipsisCount(lineIndex: Int): Int = layout.getLineEllipsisCount(lineIndex)
+
override fun getLineForOffset(offset: Int): Int = layout.getLineForOffset(offset)
override fun getHorizontalPosition(offset: Int, usePrimaryDirection: Boolean): Float =
@@ -554,7 +596,8 @@
hyphens: Int,
breakStrategy: Int,
lineBreakStyle: Int,
- lineBreakWordStyle: Int
+ lineBreakWordStyle: Int,
+ charSequence: CharSequence = this.charSequence,
) =
TextLayout(
charSequence = charSequence,
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/TextLayout.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/TextLayout.android.kt
index 06e3aef..87c2aed 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/TextLayout.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/TextLayout.android.kt
@@ -313,7 +313,9 @@
false
} else {
/* When maxLines exceeds
- 1. if ellipsis is applied, ellipsisCount of lastLine is greater than 0.
+ 1. if ellipsis is applied, ellipsisCount of lastLine is greater than 0. It works
+ for all ellipsis position because start/middle ellipsis only supported for a single
+ line text.
2. if ellipsis is not applies, lineEnd of the last line is unequals to
charSequence.length.
On certain cases, even though ellipsize is set, text overflow might still be
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/ActualParagraph.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/ActualParagraph.android.kt
index 0cd4880..2cf1aa8 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/ActualParagraph.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/ActualParagraph.android.kt
@@ -29,6 +29,7 @@
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.createFontFamilyResolver
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
@@ -62,7 +63,7 @@
density = density
),
maxLines,
- ellipsis,
+ if (ellipsis) TextOverflow.Ellipsis else TextOverflow.Clip,
Constraints(maxWidth = width.ceilToInt())
)
@@ -72,7 +73,7 @@
spanStyles: List<AnnotatedString.Range<SpanStyle>>,
placeholders: List<AnnotatedString.Range<Placeholder>>,
maxLines: Int,
- ellipsis: Boolean,
+ overflow: TextOverflow,
constraints: Constraints,
density: Density,
fontFamilyResolver: FontFamily.Resolver
@@ -87,19 +88,19 @@
density = density
),
maxLines,
- ellipsis,
+ overflow,
constraints
)
internal actual fun ActualParagraph(
paragraphIntrinsics: ParagraphIntrinsics,
maxLines: Int,
- ellipsis: Boolean,
+ overflow: TextOverflow,
constraints: Constraints
): Paragraph =
AndroidParagraph(
paragraphIntrinsics as AndroidParagraphIntrinsics,
maxLines,
- ellipsis,
+ overflow,
constraints
)
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraph.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraph.kt
index 6d265c5..fff373a 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraph.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraph.kt
@@ -33,6 +33,7 @@
import androidx.compose.ui.text.platform.drawMultiParagraph
import androidx.compose.ui.text.style.ResolvedTextDirection
import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.util.fastFlatMap
@@ -48,13 +49,14 @@
* define the width of the MultiParagraph. [Constraints.maxHeight] helps defining the number of
* lines that fit with ellipsis is true. Minimum components of the [Constraints] object are no-op.
* @param maxLines the maximum number of lines that the text can have
- * @param ellipsis whether to ellipsize text, applied only when [maxLines] is set
+ * @param overflow configures how visual overflow is handled. Ellipsis is applied only when
+ * [maxLines] is set
*/
class MultiParagraph(
val intrinsics: MultiParagraphIntrinsics,
constraints: Constraints,
val maxLines: Int = DefaultMaxLines,
- ellipsis: Boolean = false,
+ overflow: TextOverflow = TextOverflow.Clip,
) {
/**
@@ -62,6 +64,31 @@
* [ParagraphStyle]s in a given text.
*
* @param intrinsics previously calculated text intrinsics
+ * @param constraints how wide and tall the text is allowed to be. [Constraints.maxWidth] will
+ * define the width of the MultiParagraph. [Constraints.maxHeight] helps defining the number
+ * of lines that fit with ellipsis is true. Minimum components of the [Constraints] object are
+ * no-op.
+ * @param maxLines the maximum number of lines that the text can have
+ * @param ellipsis whether to ellipsize text, applied only when [maxLines] is set
+ */
+ @Deprecated("Constructor with `ellipsis: Boolean` is deprecated, pass TextOverflow instead ")
+ constructor(
+ intrinsics: MultiParagraphIntrinsics,
+ constraints: Constraints,
+ maxLines: Int = DefaultMaxLines,
+ ellipsis: Boolean,
+ ) : this(
+ intrinsics = intrinsics,
+ constraints = constraints,
+ maxLines = maxLines,
+ overflow = if (ellipsis) TextOverflow.Ellipsis else TextOverflow.Clip
+ )
+
+ /**
+ * Lays out and renders multiple paragraphs at once. Unlike [Paragraph], supports multiple
+ * [ParagraphStyle]s in a given text.
+ *
+ * @param intrinsics previously calculated text intrinsics
* @param maxLines the maximum number of lines that the text can have
* @param ellipsis whether to ellipsize text, applied only when [maxLines] is set
* @param width how wide the text is allowed to be
@@ -80,7 +107,12 @@
maxLines: Int = DefaultMaxLines,
ellipsis: Boolean = false,
width: Float
- ) : this(intrinsics, Constraints(maxWidth = width.ceilToInt()), maxLines, ellipsis)
+ ) : this(
+ intrinsics,
+ Constraints(maxWidth = width.ceilToInt()),
+ maxLines,
+ if (ellipsis) TextOverflow.Ellipsis else TextOverflow.Clip
+ )
/**
* Lays out a given [annotatedString] with the given constraints. Unlike a [Paragraph],
@@ -180,7 +212,7 @@
fontFamilyResolver = fontFamilyResolver
),
maxLines = maxLines,
- ellipsis = ellipsis,
+ overflow = if (ellipsis) TextOverflow.Ellipsis else TextOverflow.Clip,
constraints = Constraints(maxWidth = width.ceilToInt())
)
@@ -206,6 +238,7 @@
* [placeholders] crosses paragraph boundary.
* @see Placeholder
*/
+ @Deprecated("Constructor with `ellipsis: Boolean` is deprecated, pass TextOverflow instead")
constructor(
annotatedString: AnnotatedString,
style: TextStyle,
@@ -214,7 +247,7 @@
fontFamilyResolver: FontFamily.Resolver,
placeholders: List<AnnotatedString.Range<Placeholder>> = listOf(),
maxLines: Int = Int.MAX_VALUE,
- ellipsis: Boolean = false
+ ellipsis: Boolean
) : this(
intrinsics =
MultiParagraphIntrinsics(
@@ -225,7 +258,53 @@
fontFamilyResolver = fontFamilyResolver
),
maxLines = maxLines,
- ellipsis = ellipsis,
+ overflow = if (ellipsis) TextOverflow.Ellipsis else TextOverflow.Clip,
+ constraints = constraints
+ )
+
+ /**
+ * Lays out a given [annotatedString] with the given constraints. Unlike a [Paragraph],
+ * [MultiParagraph] can handle a text what has multiple paragraph styles.
+ *
+ * @param annotatedString the text to be laid out
+ * @param style the [TextStyle] to be applied to the whole text
+ * @param constraints how wide and tall the text is allowed to be. [Constraints.maxWidth] will
+ * define the width of the MultiParagraph. [Constraints.maxHeight] helps defining the number
+ * of lines that fit with ellipsis is true. Minimum components of the [Constraints] object are
+ * no-op.
+ * @param density density of the device
+ * @param fontFamilyResolver to be used to load the font given in [SpanStyle]s
+ * @param placeholders a list of [Placeholder]s that specify ranges of text which will be
+ * skipped during layout and replaced with [Placeholder]. It's required that the range of each
+ * [Placeholder] doesn't cross paragraph boundary, otherwise [IllegalArgumentException] is
+ * thrown.
+ * @param maxLines the maximum number of lines that the text can have
+ * @param overflow configures how visual overflow is handled. Ellipsis is applied only when
+ * [maxLines] is set
+ * @throws IllegalArgumentException if [ParagraphStyle.textDirection] is not set, or any of the
+ * [placeholders] crosses paragraph boundary.
+ * @see Placeholder
+ */
+ constructor(
+ annotatedString: AnnotatedString,
+ style: TextStyle,
+ constraints: Constraints,
+ density: Density,
+ fontFamilyResolver: FontFamily.Resolver,
+ placeholders: List<AnnotatedString.Range<Placeholder>> = listOf(),
+ maxLines: Int = Int.MAX_VALUE,
+ overflow: TextOverflow = TextOverflow.Clip
+ ) : this(
+ intrinsics =
+ MultiParagraphIntrinsics(
+ annotatedString = annotatedString,
+ style = style,
+ placeholders = placeholders,
+ density = density,
+ fontFamilyResolver = fontFamilyResolver
+ ),
+ maxLines = maxLines,
+ overflow = overflow,
constraints = constraints
)
@@ -253,7 +332,7 @@
/**
* The amount of vertical space this paragraph occupies.
*
- * Valid only after [layout] has been called.
+ * Valid only after layout has been called.
*/
val height: Float
@@ -326,7 +405,7 @@
}
),
maxLines - currentLineCount,
- ellipsis,
+ overflow,
)
val paragraphTop = currentHeight
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Paragraph.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Paragraph.kt
index 81cec32a..36ba20c 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Paragraph.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Paragraph.kt
@@ -34,6 +34,7 @@
import androidx.compose.ui.text.platform.ActualParagraph
import androidx.compose.ui.text.style.ResolvedTextDirection
import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import kotlin.math.ceil
@@ -472,7 +473,7 @@
spanStyles,
placeholders,
maxLines,
- ellipsis,
+ if (ellipsis) TextOverflow.Ellipsis else TextOverflow.Clip,
Constraints(maxWidth = width.ceilToInt()),
density,
fontFamilyResolver
@@ -499,6 +500,7 @@
* @param ellipsis whether to ellipsize text, applied only when [maxLines] is set
* @throws IllegalArgumentException if [ParagraphStyle.textDirection] is not set
*/
+@Deprecated("Paragraph that takes `ellipsis: Boolean` is deprecated, pass TextOverflow instead.")
fun Paragraph(
text: String,
style: TextStyle,
@@ -508,7 +510,7 @@
spanStyles: List<AnnotatedString.Range<SpanStyle>> = listOf(),
placeholders: List<AnnotatedString.Range<Placeholder>> = listOf(),
maxLines: Int = DefaultMaxLines,
- ellipsis: Boolean = false
+ ellipsis: Boolean
): Paragraph =
ActualParagraph(
text,
@@ -516,7 +518,51 @@
spanStyles,
placeholders,
maxLines,
- ellipsis,
+ if (ellipsis) TextOverflow.Ellipsis else TextOverflow.Clip,
+ constraints,
+ density,
+ fontFamilyResolver
+ )
+
+/**
+ * Lays out a given [text] with the given constraints. A paragraph is a text that has a single
+ * [ParagraphStyle].
+ *
+ * If the [style] does not contain any [androidx.compose.ui.text.style.TextDirection],
+ * [androidx.compose.ui.text.style.TextDirection.Content] is used as the default value.
+ *
+ * @param text the text to be laid out
+ * @param style the [TextStyle] to be applied to the whole text
+ * @param constraints how wide and tall the text is allowed to be. [Constraints.maxWidth] will
+ * define the width of the Paragraph. [Constraints.maxHeight] helps defining the number of lines
+ * that fit with ellipsis is true. Minimum components of the [Constraints] object are no-op.
+ * @param density density of the device
+ * @param fontFamilyResolver [FontFamily.Resolver] to be used to load the font given in [SpanStyle]s
+ * @param spanStyles [SpanStyle]s to be applied to parts of text
+ * @param placeholders a list of placeholder metrics which tells [Paragraph] where should be left
+ * blank to leave space for inline elements.
+ * @param maxLines the maximum number of lines that the text can have
+ * @param overflow specifies how visual overflow should be handled
+ * @throws IllegalArgumentException if [ParagraphStyle.textDirection] is not set
+ */
+fun Paragraph(
+ text: String,
+ style: TextStyle,
+ constraints: Constraints,
+ density: Density,
+ fontFamilyResolver: FontFamily.Resolver,
+ spanStyles: List<AnnotatedString.Range<SpanStyle>> = listOf(),
+ placeholders: List<AnnotatedString.Range<Placeholder>> = listOf(),
+ maxLines: Int = DefaultMaxLines,
+ overflow: TextOverflow = TextOverflow.Clip
+): Paragraph =
+ ActualParagraph(
+ text,
+ style,
+ spanStyles,
+ placeholders,
+ maxLines,
+ overflow,
constraints,
density,
fontFamilyResolver
@@ -549,7 +595,7 @@
ActualParagraph(
paragraphIntrinsics,
maxLines,
- ellipsis,
+ if (ellipsis) TextOverflow.Ellipsis else TextOverflow.Clip,
Constraints(maxWidth = width.ceilToInt())
)
@@ -564,11 +610,42 @@
* @param maxLines the maximum number of lines that the text can have
* @param ellipsis whether to ellipsize text, applied only when [maxLines] is set
*/
+@Deprecated(
+ "Paragraph that takes ellipsis: Boolean is deprecated, pass TextOverflow instead.",
+ ReplaceWith(
+ "Paragraph(paragraphIntrinsics, constraints, maxLines, " +
+ "if (ellipsis) TextOverflow.Ellipsis else TextOverflow.Clip"
+ )
+)
fun Paragraph(
paragraphIntrinsics: ParagraphIntrinsics,
constraints: Constraints,
maxLines: Int = DefaultMaxLines,
- ellipsis: Boolean = false
-): Paragraph = ActualParagraph(paragraphIntrinsics, maxLines, ellipsis, constraints)
+ ellipsis: Boolean
+): Paragraph =
+ ActualParagraph(
+ paragraphIntrinsics,
+ maxLines,
+ if (ellipsis) TextOverflow.Ellipsis else TextOverflow.Clip,
+ constraints
+ )
+
+/**
+ * Lays out the text in [ParagraphIntrinsics] with the given constraints. A paragraph is a text that
+ * has a single [ParagraphStyle].
+ *
+ * @param paragraphIntrinsics [ParagraphIntrinsics] instance
+ * @param constraints how wide and tall the text is allowed to be. [Constraints.maxWidth] will
+ * define the width of the Paragraph. [Constraints.maxHeight] helps defining the number of lines
+ * that fit with ellipsis is true. Minimum components of the [Constraints] object are no-op.
+ * @param maxLines the maximum number of lines that the text can have
+ * @param overflow specifies how visual overflow should be handled
+ */
+fun Paragraph(
+ paragraphIntrinsics: ParagraphIntrinsics,
+ constraints: Constraints,
+ maxLines: Int = DefaultMaxLines,
+ overflow: TextOverflow = TextOverflow.Clip
+): Paragraph = ActualParagraph(paragraphIntrinsics, maxLines, overflow, constraints)
internal fun Float.ceilToInt(): Int = ceil(this).toInt()
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt
index 5cbc366..cd65c98 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt
@@ -277,7 +277,7 @@
)
val minWidth = constraints.minWidth
- val widthMatters = softWrap || overflow == TextOverflow.Ellipsis
+ val widthMatters = softWrap || overflow.isEllipsis
val maxWidth =
if (widthMatters && constraints.hasBoundedWidth) {
constraints.maxWidth
@@ -300,7 +300,7 @@
// AA…
// Here we assume there won't be any '\n' character when softWrap is false. And make
// maxLines 1 to implement the similar behavior.
- val overwriteMaxLines = !softWrap && overflow == TextOverflow.Ellipsis
+ val overwriteMaxLines = !softWrap && overflow.isEllipsis
val finalMaxLines = if (overwriteMaxLines) 1 else maxLines
// if minWidth == maxWidth the width is fixed.
@@ -332,7 +332,7 @@
),
// This is a fallback behavior for ellipsis. Native
maxLines = finalMaxLines,
- ellipsis = overflow == TextOverflow.Ellipsis
+ overflow = overflow
)
return TextLayoutResult(
@@ -447,3 +447,10 @@
return true
}
}
+
+private val TextOverflow.isEllipsis: Boolean
+ get() {
+ return this == TextOverflow.Ellipsis ||
+ this == TextOverflow.StartEllipsis ||
+ this == TextOverflow.MiddleEllipsis
+ }
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/platform/PlatformParagraph.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/platform/PlatformParagraph.kt
index 3651805..c590354 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/platform/PlatformParagraph.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/platform/PlatformParagraph.kt
@@ -23,6 +23,7 @@
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
@@ -54,7 +55,7 @@
spanStyles: List<AnnotatedString.Range<SpanStyle>>,
placeholders: List<AnnotatedString.Range<Placeholder>>,
maxLines: Int,
- ellipsis: Boolean,
+ overflow: TextOverflow,
constraints: Constraints,
density: Density,
fontFamilyResolver: FontFamily.Resolver
@@ -64,7 +65,7 @@
internal expect fun ActualParagraph(
paragraphIntrinsics: ParagraphIntrinsics,
maxLines: Int,
- ellipsis: Boolean,
+ overflow: TextOverflow,
constraints: Constraints
): Paragraph
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/TextOverflow.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/TextOverflow.kt
index 97dc5fa..8290cc6 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/TextOverflow.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/TextOverflow.kt
@@ -26,7 +26,9 @@
return when (this) {
Clip -> "Clip"
Ellipsis -> "Ellipsis"
+ MiddleEllipsis -> "MiddleEllipsis"
Visible -> "Visible"
+ StartEllipsis -> "StartEllipsis"
else -> "Invalid"
}
}
@@ -40,7 +42,9 @@
@Stable val Clip = TextOverflow(1)
/**
- * Use an ellipsis to indicate that the text has overflowed.
+ * Use an ellipsis at the end of the string to indicate that the text has overflowed.
+ *
+ * For example, [This is a ...].
*
* @sample androidx.compose.ui.text.samples.TextOverflowEllipsisSample
*/
@@ -66,5 +70,29 @@
* such as `Modifier.clipToBounds`.
*/
@Stable val Visible = TextOverflow(3)
+
+ /**
+ * Use an ellipsis at the start of the string to indicate that the text has overflowed.
+ *
+ * For example, [... is a text].
+ *
+ * Note that not all platforms support the ellipsis at the start. For example, on Android
+ * the start ellipsis is only available for a single line text (i.e. when either a soft wrap
+ * is disabled or a maximum number of lines maxLines set to 1). In case of multiline text it
+ * will fallback to [Clip].
+ */
+ @Stable val StartEllipsis = TextOverflow(4)
+
+ /**
+ * Use an ellipsis in the middle of the string to indicate that the text has overflowed.
+ *
+ * For example, [This ... text].
+ *
+ * Note that not all platforms support the ellipsis in the middle. For example, on Android
+ * the middle ellipsis is only available for a single line text (i.e. when either a soft
+ * wrap is disabled or a maximum number of lines maxLines set to 1). In case of multiline
+ * text it will fallback to [Clip].
+ */
+ @Stable val MiddleEllipsis = TextOverflow(5)
}
}
diff --git a/compose/ui/ui-text/src/commonStubsMain/kotlin/androidx/compose/ui/text/platform/SkiaParagraph.commonStubs.kt b/compose/ui/ui-text/src/commonStubsMain/kotlin/androidx/compose/ui/text/platform/SkiaParagraph.commonStubs.kt
index fb9de57..a624af7 100644
--- a/compose/ui/ui-text/src/commonStubsMain/kotlin/androidx/compose/ui/text/platform/SkiaParagraph.commonStubs.kt
+++ b/compose/ui/ui-text/src/commonStubsMain/kotlin/androidx/compose/ui/text/platform/SkiaParagraph.commonStubs.kt
@@ -24,6 +24,7 @@
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.implementedInJetBrainsFork
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
@@ -45,7 +46,7 @@
spanStyles: List<Range<SpanStyle>>,
placeholders: List<Range<Placeholder>>,
maxLines: Int,
- ellipsis: Boolean,
+ overflow: TextOverflow,
constraints: Constraints,
density: Density,
fontFamilyResolver: FontFamily.Resolver
@@ -54,6 +55,6 @@
internal actual fun ActualParagraph(
paragraphIntrinsics: ParagraphIntrinsics,
maxLines: Int,
- ellipsis: Boolean,
+ overflow: TextOverflow,
constraints: Constraints
): Paragraph = implementedInJetBrainsFork()
diff --git a/compose/ui/ui-unit/api/current.txt b/compose/ui/ui-unit/api/current.txt
index c372803..b0debd1 100644
--- a/compose/ui/ui-unit/api/current.txt
+++ b/compose/ui/ui-unit/api/current.txt
@@ -226,7 +226,9 @@
}
public static final class IntOffset.Companion {
+ method public long getMax();
method public long getZero();
+ property public final long Max;
property public final long Zero;
}
diff --git a/compose/ui/ui-unit/api/restricted_current.txt b/compose/ui/ui-unit/api/restricted_current.txt
index 6f13e9f..4fc0dc1 100644
--- a/compose/ui/ui-unit/api/restricted_current.txt
+++ b/compose/ui/ui-unit/api/restricted_current.txt
@@ -226,7 +226,9 @@
}
public static final class IntOffset.Companion {
+ method public long getMax();
method public long getZero();
+ property public final long Max;
property public final long Zero;
}
diff --git a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/IntOffset.kt b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/IntOffset.kt
index ca4caa3..89bf3f1 100644
--- a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/IntOffset.kt
+++ b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/IntOffset.kt
@@ -137,6 +137,7 @@
companion object {
val Zero = IntOffset(0x0L)
+ val Max = IntOffset(0x7FFF_FFFF_7FFF_FFFF)
}
}
diff --git a/compose/ui/ui/api/current.ignore b/compose/ui/ui/api/current.ignore
index 074fe37..8caaf36 100644
--- a/compose/ui/ui/api/current.ignore
+++ b/compose/ui/ui/api/current.ignore
@@ -13,5 +13,9 @@
Removed method androidx.compose.ui.semantics.SemanticsProperties.getInvisibleToUser() from compatibility checked API surface
+ChangedType: androidx.compose.ui.layout.MeasureResult#getAlignmentLines():
+ Method androidx.compose.ui.layout.MeasureResult.getAlignmentLines has changed return type from java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> to java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer>
+
+
RemovedMethod: androidx.compose.ui.layout.LayoutCoordinates#transformToScreen(float[]):
Removed method androidx.compose.ui.layout.LayoutCoordinates.transformToScreen(float[])
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index cc929b6..738fb68 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -131,6 +131,11 @@
method public <R> R foldOut(R initial, kotlin.jvm.functions.Function2<? super androidx.compose.ui.Modifier.Element,? super R,? extends R> operation);
}
+ @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public final class ComposeUiFlags {
+ field public static final androidx.compose.ui.ComposeUiFlags INSTANCE;
+ field public static boolean isRectTrackingEnabled;
+ }
+
public final class ComposedModifierKt {
method public static androidx.compose.ui.Modifier composed(androidx.compose.ui.Modifier, String fullyQualifiedName, Object? key1, Object? key2, Object? key3, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.platform.InspectorInfo,kotlin.Unit> inspectorInfo, kotlin.jvm.functions.Function1<? super androidx.compose.ui.Modifier,? extends androidx.compose.ui.Modifier> factory);
method public static androidx.compose.ui.Modifier composed(androidx.compose.ui.Modifier, String fullyQualifiedName, Object? key1, Object? key2, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.platform.InspectorInfo,kotlin.Unit> inspectorInfo, kotlin.jvm.functions.Function1<? super androidx.compose.ui.Modifier,? extends androidx.compose.ui.Modifier> factory);
@@ -2338,20 +2343,20 @@
}
public interface MeasureResult {
- method public java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> getAlignmentLines();
+ method public java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> getAlignmentLines();
method public int getHeight();
method public default kotlin.jvm.functions.Function1<androidx.compose.ui.layout.RulerScope,kotlin.Unit>? getRulers();
method public int getWidth();
method public void placeChildren();
- property public abstract java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines;
+ property public abstract java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines;
property public abstract int height;
property public default kotlin.jvm.functions.Function1<androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers;
property public abstract int width;
}
@androidx.compose.ui.layout.MeasureScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface MeasureScope extends androidx.compose.ui.layout.IntrinsicMeasureScope {
- method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
- method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
+ method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
+ method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
}
@kotlin.DslMarker public @interface MeasureScopeMarker {
diff --git a/compose/ui/ui/api/restricted_current.ignore b/compose/ui/ui/api/restricted_current.ignore
index 074fe37..8caaf36 100644
--- a/compose/ui/ui/api/restricted_current.ignore
+++ b/compose/ui/ui/api/restricted_current.ignore
@@ -13,5 +13,9 @@
Removed method androidx.compose.ui.semantics.SemanticsProperties.getInvisibleToUser() from compatibility checked API surface
+ChangedType: androidx.compose.ui.layout.MeasureResult#getAlignmentLines():
+ Method androidx.compose.ui.layout.MeasureResult.getAlignmentLines has changed return type from java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> to java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer>
+
+
RemovedMethod: androidx.compose.ui.layout.LayoutCoordinates#transformToScreen(float[]):
Removed method androidx.compose.ui.layout.LayoutCoordinates.transformToScreen(float[])
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 2c9b7cf..41d810c 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -131,6 +131,11 @@
method public <R> R foldOut(R initial, kotlin.jvm.functions.Function2<? super androidx.compose.ui.Modifier.Element,? super R,? extends R> operation);
}
+ @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public final class ComposeUiFlags {
+ field public static final androidx.compose.ui.ComposeUiFlags INSTANCE;
+ field public static boolean isRectTrackingEnabled;
+ }
+
public final class ComposedModifierKt {
method public static androidx.compose.ui.Modifier composed(androidx.compose.ui.Modifier, String fullyQualifiedName, Object? key1, Object? key2, Object? key3, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.platform.InspectorInfo,kotlin.Unit> inspectorInfo, kotlin.jvm.functions.Function1<? super androidx.compose.ui.Modifier,? extends androidx.compose.ui.Modifier> factory);
method public static androidx.compose.ui.Modifier composed(androidx.compose.ui.Modifier, String fullyQualifiedName, Object? key1, Object? key2, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.platform.InspectorInfo,kotlin.Unit> inspectorInfo, kotlin.jvm.functions.Function1<? super androidx.compose.ui.Modifier,? extends androidx.compose.ui.Modifier> factory);
@@ -2341,20 +2346,20 @@
}
public interface MeasureResult {
- method public java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> getAlignmentLines();
+ method public java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> getAlignmentLines();
method public int getHeight();
method public default kotlin.jvm.functions.Function1<androidx.compose.ui.layout.RulerScope,kotlin.Unit>? getRulers();
method public int getWidth();
method public void placeChildren();
- property public abstract java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines;
+ property public abstract java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines;
property public abstract int height;
property public default kotlin.jvm.functions.Function1<androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers;
property public abstract int width;
}
@androidx.compose.ui.layout.MeasureScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface MeasureScope extends androidx.compose.ui.layout.IntrinsicMeasureScope {
- method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
- method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
+ method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
+ method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
}
@kotlin.DslMarker public @interface MeasureScopeMarker {
diff --git a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/NestedScrollingBenchmark.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/NestedScrollingBenchmark.kt
index dfd58d7..fb9f91c 100644
--- a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/NestedScrollingBenchmark.kt
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/NestedScrollingBenchmark.kt
@@ -34,9 +34,7 @@
import androidx.test.filters.LargeTest
import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread
import kotlinx.coroutines.runBlocking
-import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -48,7 +46,6 @@
private val nestedScrollingCaseFactory = { NestedScrollingTestCase() }
- @Ignore("b/362302352")
@Test
fun nested_scroll_propagation() {
benchmarkRule.runBenchmarkFor(nestedScrollingCaseFactory) {
@@ -101,7 +98,6 @@
private val noOpConnection = object : NestedScrollConnection {}
private val delta = Offset(200f, 200f)
private val velocity = Velocity(2000f, 200f)
- private var scrollResult = Offset.Zero
private var velocityResult = Velocity.Zero
private val IntermediateConnection = object : NestedScrollConnection {}
@@ -129,9 +125,8 @@
}
override fun toggleState() {
- scrollResult = dispatcher.dispatchPreScroll(delta, NestedScrollSource.UserInput)
- scrollResult =
- dispatcher.dispatchPostScroll(delta, scrollResult, NestedScrollSource.UserInput)
+ val scrollResult = dispatcher.dispatchPreScroll(delta, NestedScrollSource.UserInput)
+ dispatcher.dispatchPostScroll(delta, scrollResult, NestedScrollSource.UserInput)
runBlocking {
velocityResult = dispatcher.dispatchPreFling(velocity)
@@ -144,8 +139,5 @@
assertNotEquals(collectedDeltasMiddle, Offset.Zero)
assertNotEquals(collectedVelocityOuter, Velocity.Zero)
assertNotEquals(collectedVelocityMiddle, Velocity.Zero)
-
- assertEquals(scrollResult, collectedDeltasOuter)
- assertEquals(velocityResult, collectedVelocityOuter)
}
}
diff --git a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/spatial/RectListBenchmark.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/spatial/RectListBenchmark.kt
new file mode 100644
index 0000000..957df79
--- /dev/null
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/spatial/RectListBenchmark.kt
@@ -0,0 +1,246 @@
+/*
+ * 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:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+
+package androidx.compose.ui.benchmark.spatial
+
+import androidx.benchmark.junit4.BenchmarkRule
+import androidx.benchmark.junit4.measureRepeated
+import androidx.collection.mutableIntListOf
+import androidx.compose.ui.spatial.RectList
+import androidx.compose.ui.util.fastForEach
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import kotlin.math.max
+import kotlin.random.Random
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class RectListBenchmark {
+
+ @get:Rule val rule = BenchmarkRule()
+
+ private fun construct() = RectList()
+
+ @Test
+ fun b01_insertExampleDataLinear() {
+ val testData = exampleLayoutRects
+ rule.measureRepeated {
+ val qt = construct()
+ for (i in testData.indices) {
+ val rect = testData[i]
+ qt.insert(
+ i,
+ rect[0],
+ rect[1],
+ rect[2],
+ rect[3],
+ -1,
+ false,
+ )
+ }
+ }
+ }
+
+ private fun insertRecursive(qt: RectList, item: Item, scrollableId: Int) {
+ val bounds = item.bounds
+
+ qt.insert(
+ item.id,
+ bounds[0],
+ bounds[1],
+ bounds[2],
+ bounds[3],
+ scrollableId,
+ item.scrollable,
+ )
+ item.children.fastForEach {
+ insertRecursive(qt, it, if (item.scrollable) item.id else scrollableId)
+ }
+ }
+
+ @Test
+ fun b01_insertExampleData() {
+ val item = rootItem
+ rule.measureRepeated {
+ val qt = construct()
+ insertRecursive(qt, item, -1)
+ }
+ }
+
+ @Test
+ fun b02_removeExampleData() {
+ val testData = exampleLayoutRects
+ rule.measureRepeated {
+ val grid = runWithTimingDisabled {
+ val qt = construct()
+ insertRecursive(qt, rootItem, -1)
+ qt
+ }
+ for (i in testData.indices) {
+ grid.remove(i)
+ }
+ }
+ }
+
+ @Test
+ fun b03_updateExampleItems() {
+ val testData = exampleLayoutRects
+ val r = Random(1234)
+ rule.measureRepeated {
+ val qt = runWithTimingDisabled {
+ val qt = construct()
+ insertRecursive(qt, rootItem, -1)
+ qt
+ }
+ for (i in testData.indices) {
+ val rect = testData[i]
+ val x = r.nextInt(-100, 100)
+ val y = r.nextInt(-100, 100)
+ qt.update(
+ i,
+ max(rect[0] + x, 0),
+ max(rect[1] + y, 0),
+ max(rect[2] + x, 0),
+ max(rect[3] + y, 0),
+ )
+ }
+ }
+ }
+
+ @Test
+ fun b04_updateScrollableContainer() {
+ val scrollableItems = scrollableItems
+ val r = Random(1234)
+ rule.measureRepeated {
+ val qt = runWithTimingDisabled {
+ val qt = construct()
+ insertRecursive(qt, rootItem, -1)
+ qt
+ }
+ scrollableItems.fastForEach {
+ val x = r.nextInt(-100, 100)
+ val y = r.nextInt(-100, 100)
+ qt.updateSubhierarchy(it.id, x, y)
+ }
+ }
+ }
+
+ @Test
+ fun b05_findOccludingRectsExampleItems() {
+ val queries = occludingRectQueries
+ rule.measureRepeated {
+ val qt = runWithTimingDisabled {
+ val qt = construct()
+ insertRecursive(qt, rootItem, -1)
+ qt
+ }
+ for (i in queries.indices) {
+ val list = runWithTimingDisabled { mutableIntListOf() }
+ val bounds = queries[i]
+ qt.forEachIntersection(
+ bounds[0],
+ bounds[1],
+ bounds[2],
+ bounds[3],
+ ) {
+ runWithTimingDisabled { list.add(it) }
+ }
+ }
+ }
+ }
+
+ @Test
+ fun b06_findKNearestNeighborsInDirection() {
+ val queries = nearestNeighborQueries
+ val numberOfResults = 4
+ rule.measureRepeated {
+ val qt = runWithTimingDisabled {
+ val qt = construct()
+ insertRecursive(qt, rootItem, -1)
+ qt
+ }
+ for (i in queries.indices) {
+ for (direction in 1..4) {
+ val list = runWithTimingDisabled { mutableIntListOf() }
+ val bounds = queries[i]
+ qt.findKNearestNeighbors(
+ direction,
+ numberOfResults,
+ bounds[0],
+ bounds[1],
+ bounds[2],
+ bounds[3],
+ ) { _, id, _, _, _, _ ->
+ runWithTimingDisabled { list.add(id) }
+ }
+ }
+ }
+ }
+ }
+
+ @Test
+ fun b06_findNearestNeighborInDirection() {
+ val queries = nearestNeighborQueries
+ rule.measureRepeated {
+ val qt = runWithTimingDisabled {
+ val qt = construct()
+ insertRecursive(qt, rootItem, -1)
+ qt
+ }
+ for (i in queries.indices) {
+ for (direction in 1..4) {
+ val list = runWithTimingDisabled { mutableIntListOf() }
+ val bounds = queries[i]
+ val result =
+ qt.findNearestNeighbor(
+ direction,
+ bounds[0],
+ bounds[1],
+ bounds[2],
+ bounds[3],
+ )
+ runWithTimingDisabled { list.add(result) }
+ }
+ }
+ }
+ }
+
+ @Test
+ fun b07_findEligiblePointerInputs() {
+ val queries = pointerInputQueries
+ rule.measureRepeated {
+ val qt = runWithTimingDisabled {
+ val qt = construct()
+ insertRecursive(qt, rootItem, -1)
+ qt
+ }
+ for (i in queries.indices) {
+ val list = runWithTimingDisabled { mutableIntListOf() }
+ val bounds = queries[i]
+ qt.forEachIntersection(
+ bounds[0],
+ bounds[1],
+ ) {
+ runWithTimingDisabled { list.add(it) }
+ }
+ }
+ }
+ }
+}
diff --git a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/spatial/SpatialTestData.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/spatial/SpatialTestData.kt
new file mode 100644
index 0000000..ece8059
--- /dev/null
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/spatial/SpatialTestData.kt
@@ -0,0 +1,2309 @@
+/*
+ * 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.benchmark.spatial
+
+val occludingRectQueries =
+ arrayOf(
+ intArrayOf(490, 2073, 945, 2693),
+ intArrayOf(84, 2777, 1356, 2993),
+ intArrayOf(966, 2074, 1379, 2703),
+ intArrayOf(84, 1548, 1356, 1821),
+ intArrayOf(35, 2073, 490, 2693),
+ )
+
+val nearestNeighborQueries =
+ arrayOf(
+ intArrayOf(56, 2074, 469, 2703), // left side app image
+ intArrayOf(288, 2812, 576, 3036), // bottom nav bar middle button
+ intArrayOf(1048, 478, 1187, 548), // install button
+ intArrayOf(983, 1580, 1300, 1790), // show button
+ intArrayOf(1258, 1933, 1342, 2017), // more icon
+ )
+
+val pointerInputQueries =
+ arrayOf(
+ intArrayOf(1120, 1654), // Show button
+ intArrayOf(1263, 1943), // three dots button
+ intArrayOf(615, 2312), // app image
+ intArrayOf(1100, 496), // install button
+ intArrayOf(710, 215), // search bar
+ )
+
+class Item(
+ val id: Int,
+ val bounds: IntArray,
+ val scrollable: Boolean,
+ val focusable: Boolean,
+ val pointerInput: Boolean,
+) {
+ val children: MutableList<Item> = mutableListOf()
+
+ operator fun Item.unaryPlus() {
+ @Suppress("LABEL_RESOLVE_WILL_CHANGE") [email protected](this)
+ }
+}
+
+fun Item(
+ id: Int,
+ l: Int,
+ t: Int,
+ r: Int,
+ b: Int,
+ scrollable: Boolean,
+ focusable: Boolean,
+ pointerInput: Boolean,
+): Item = Item(id, intArrayOf(l, t, r, b), scrollable, focusable, pointerInput)
+
+fun Item(
+ id: Int,
+ l: Int,
+ t: Int,
+ r: Int,
+ b: Int,
+ scrollable: Boolean,
+ focusable: Boolean,
+ pointerInput: Boolean,
+ scope: Item.() -> Unit
+): Item {
+ return Item(id, intArrayOf(l, t, r, b), scrollable, focusable, pointerInput).apply { scope() }
+}
+
+val rootItem =
+ Item(0, 0, 0, 1440, 3120, false, false, false) {
+ +Item(1, 0, 0, 1440, 3120, false, false, false) {
+ +Item(2, 0, 0, 1, 1, false, false, false)
+ +Item(3, 0, 0, 1440, 3120, false, false, false) {
+ +Item(4, 0, 0, 1440, 3120, false, false, false) {
+ +Item(5, 0, 0, 1440, 3120, false, false, false) {
+ +Item(6, 0, 0, 1440, 3120, false, false, false) {
+ +Item(7, 0, 0, 1440, 3120, false, false, false) {
+ +Item(8, 0, 0, 1440, 3120, false, false, false) {
+ +Item(9, 0, 0, 1440, 3120, false, false, false) {
+ +Item(10, 0, 0, 1440, 3036, false, false, false) {
+ +Item(11, 0, 0, 1440, 2812, false, false, false) {
+ +Item(12, 0, 0, 1440, 2812, false, false, false) {
+ +Item(
+ 13,
+ 0,
+ 0,
+ 1440,
+ 2812,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 14,
+ 0,
+ 145,
+ 1440,
+ 2812,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 15,
+ 0,
+ 145,
+ 1440,
+ 2812,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 16,
+ 0,
+ 145,
+ 1440,
+ 373,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 17,
+ 0,
+ 145,
+ 1440,
+ 369,
+ false,
+ false,
+ true
+ ) {
+ +Item(
+ 18,
+ 0,
+ 145,
+ 1440,
+ 369,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 19,
+ 14,
+ 173,
+ 182,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 20,
+ 14,
+ 173,
+ 182,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 21,
+ 14,
+ 173,
+ 182,
+ 341,
+ false,
+ false,
+ true
+ ) {
+ +Item(
+ 22,
+ 14,
+ 173,
+ 182,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 23,
+ 28,
+ 187,
+ 168,
+ 327,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 24,
+ 56,
+ 215,
+ 140,
+ 299,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ }
+ +Item(
+ 25,
+ 196,
+ -1076,
+ 1076,
+ 1591,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 26,
+ 196,
+ -1076,
+ 1076,
+ 1591,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 27,
+ 196,
+ 216,
+ 337,
+ 300,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 28,
+ 1090,
+ 173,
+ 1426,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 29,
+ 1090,
+ 173,
+ 1426,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 30,
+ 1090,
+ 173,
+ 1426,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 31,
+ 1090,
+ 173,
+ 1426,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 32,
+ 1090,
+ 173,
+ 1258,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 33,
+ 1090,
+ 173,
+ 1258,
+ 341,
+ false,
+ false,
+ true
+ ) {
+ +Item(
+ 34,
+ 1090,
+ 173,
+ 1258,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 35,
+ 1104,
+ 187,
+ 1244,
+ 327,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 36,
+ 1132,
+ 215,
+ 1216,
+ 299,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ +Item(
+ 37,
+ 1258,
+ 173,
+ 1426,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 38,
+ 1258,
+ 173,
+ 1426,
+ 341,
+ false,
+ false,
+ true
+ ) {
+ +Item(
+ 39,
+ 1258,
+ 173,
+ 1426,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 40,
+ 1272,
+ 187,
+ 1412,
+ 327,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 41,
+ 1300,
+ 215,
+ 1384,
+ 299,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ +Item(
+ 42,
+ 0,
+ 369,
+ 1440,
+ 373,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 43,
+ 0,
+ 373,
+ 1440,
+ 2812,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 44,
+ 0,
+ 373,
+ 1440,
+ 2812,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 45,
+ 0,
+ 373,
+ 1440,
+ 2812,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 46,
+ 0,
+ 373,
+ 1440,
+ 2812,
+ true,
+ false,
+ true
+ ) {
+ +Item(
+ 47,
+ 0,
+ 1877,
+ 1440,
+ 2735,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 48,
+ 0,
+ 1877,
+ 1440,
+ 2693,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 49,
+ 84,
+ 1877,
+ 1356,
+ 2073,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 50,
+ 84,
+ 1947,
+ 354,
+ 2003,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 51,
+ 84,
+ 1947,
+ 302,
+ 2003,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 52,
+ 354,
+ 1933,
+ 1188,
+ 2017,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 53,
+ 354,
+ 1933,
+ 1012,
+ 2017,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 54,
+ 354,
+ 1933,
+ 1012,
+ 2017,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 55,
+ 1258,
+ 1933,
+ 1342,
+ 2017,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 56,
+ 1258,
+ 1933,
+ 1342,
+ 2017,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 57,
+ 0,
+ 2073,
+ 1440,
+ 2693,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 58,
+ 0,
+ 2073,
+ 1440,
+ 2693,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 59,
+ 0,
+ 2073,
+ 1440,
+ 2693,
+ true,
+ false,
+ true
+ ) {
+ +Item(
+ 60,
+ 945,
+ 2073,
+ 1400,
+ 2693,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 61,
+ 966,
+ 2074,
+ 1379,
+ 2703,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 62,
+ 966,
+ 2074,
+ 1379,
+ 2487,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 63,
+ 966,
+ 2074,
+ 1379,
+ 2487,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 64,
+ 966,
+ 2515,
+ 1379,
+ 2703,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 65,
+ 490,
+ 2073,
+ 945,
+ 2693,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 66,
+ 511,
+ 2074,
+ 924,
+ 2703,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 67,
+ 511,
+ 2074,
+ 924,
+ 2487,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 68,
+ 511,
+ 2074,
+ 924,
+ 2487,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 69,
+ 511,
+ 2515,
+ 924,
+ 2703,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 70,
+ 1400,
+ 2073,
+ 1855,
+ 2693,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 71,
+ 1421,
+ 2074,
+ 1834,
+ 2703,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 72,
+ 1421,
+ 2074,
+ 1834,
+ 2487,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 73,
+ 1421,
+ 2074,
+ 1834,
+ 2487,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 74,
+ 1421,
+ 2515,
+ 1834,
+ 2703,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 75,
+ 35,
+ 2073,
+ 490,
+ 2693,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 76,
+ 56,
+ 2074,
+ 469,
+ 2703,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 77,
+ 56,
+ 2074,
+ 469,
+ 2487,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 78,
+ 56,
+ 2074,
+ 469,
+ 2487,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 79,
+ 56,
+ 2515,
+ 469,
+ 2703,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ +Item(
+ 80,
+ 0,
+ 2777,
+ 1440,
+ 2993,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 81,
+ 84,
+ 2777,
+ 1356,
+ 2993,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 82,
+ 84,
+ 2777,
+ 1188,
+ 2993,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 83,
+ 84,
+ 2777,
+ 280,
+ 2973,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 84,
+ 84,
+ 2777,
+ 280,
+ 2973,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 85,
+ 336,
+ 2777,
+ 1188,
+ 2993,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 86,
+ 1188,
+ 2777,
+ 1356,
+ 2945,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 87,
+ 1272,
+ 2826,
+ 1356,
+ 2910,
+ false,
+ true,
+ false
+ )
+ }
+ }
+ }
+ +Item(
+ 88,
+ 0,
+ 373,
+ 1440,
+ 1849,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 89,
+ 84,
+ 429,
+ 1356,
+ 645,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 90,
+ 84,
+ 436,
+ 280,
+ 632,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 91,
+ 84,
+ 436,
+ 280,
+ 632,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 92,
+ 336,
+ 436,
+ 950,
+ 652,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 93,
+ 992,
+ 429,
+ 1356,
+ 639,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 94,
+ 992,
+ 429,
+ 1356,
+ 597,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 95,
+ 992,
+ 429,
+ 1356,
+ 597,
+ false,
+ false,
+ true
+ ) {
+ +Item(
+ 96,
+ 992,
+ 443,
+ 1356,
+ 583,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 97,
+ 1048,
+ 443,
+ 1356,
+ 583,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 98,
+ 1048,
+ 478,
+ 1187,
+ 548,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 99,
+ 1048,
+ 478,
+ 1187,
+ 548,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 100,
+ 1229,
+ 443,
+ 1356,
+ 583,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 101,
+ 1229,
+ 443,
+ 1233,
+ 583,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 102,
+ 1233,
+ 443,
+ 1356,
+ 583,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 103,
+ 1256,
+ 482,
+ 1319,
+ 545,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ +Item(
+ 104,
+ 1056,
+ 429,
+ 1293,
+ 639,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 105,
+ 1056,
+ 583,
+ 1293,
+ 639,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ +Item(
+ 106,
+ 0,
+ 687,
+ 1440,
+ 1849,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 107,
+ 0,
+ 687,
+ 1440,
+ 1849,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 108,
+ 0,
+ 687,
+ 1440,
+ 911,
+ true,
+ false,
+ false
+ ) {
+ +Item(
+ 109,
+ 0,
+ 799,
+ 84,
+ 799,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 110,
+ 84,
+ 729,
+ 377,
+ 869,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 111,
+ 171,
+ 733,
+ 291,
+ 803,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 112,
+ 171,
+ 733,
+ 242,
+ 803,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 113,
+ 249,
+ 747,
+ 291,
+ 789,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 114,
+ 84,
+ 810,
+ 377,
+ 866,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 115,
+ 338,
+ 818,
+ 377,
+ 857,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 116,
+ 377,
+ 799,
+ 433,
+ 799,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 117,
+ 433,
+ 762,
+ 437,
+ 837,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 118,
+ 437,
+ 799,
+ 493,
+ 799,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 119,
+ 493,
+ 729,
+ 839,
+ 869,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 120,
+ 631,
+ 733,
+ 701,
+ 803,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 121,
+ 631,
+ 733,
+ 701,
+ 803,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 122,
+ 493,
+ 810,
+ 839,
+ 866,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 123,
+ 799,
+ 818,
+ 838,
+ 857,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 124,
+ 839,
+ 799,
+ 895,
+ 799,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 125,
+ 895,
+ 762,
+ 899,
+ 837,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 126,
+ 899,
+ 799,
+ 955,
+ 799,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 127,
+ 955,
+ 729,
+ 1235,
+ 869,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 128,
+ 1060,
+ 733,
+ 1130,
+ 803,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 129,
+ 1060,
+ 733,
+ 1130,
+ 803,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 130,
+ 1022,
+ 810,
+ 1168,
+ 866,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 131,
+ 1129,
+ 818,
+ 1168,
+ 857,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 132,
+ 1235,
+ 799,
+ 1291,
+ 799,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 133,
+ 1291,
+ 762,
+ 1295,
+ 837,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 134,
+ 1295,
+ 799,
+ 1351,
+ 799,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 135,
+ 1351,
+ 729,
+ 1631,
+ 869,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 136,
+ 1451,
+ 733,
+ 1532,
+ 803,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 137,
+ 1451,
+ 733,
+ 1532,
+ 803,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 138,
+ 1383,
+ 810,
+ 1599,
+ 866,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 139,
+ 1631,
+ 799,
+ 1687,
+ 799,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 140,
+ 0,
+ 911,
+ 1440,
+ 1506,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 141,
+ 0,
+ 911,
+ 1440,
+ 1506,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 142,
+ 0,
+ 911,
+ 1440,
+ 1506,
+ true,
+ false,
+ true
+ ) {
+ +Item(
+ 143,
+ 1348,
+ 911,
+ 2644,
+ 1506,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 144,
+ 1380,
+ 911,
+ 2644,
+ 1506,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 145,
+ 1380,
+ 911,
+ 2644,
+ 1506,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 146,
+ 1380,
+ 911,
+ 1975,
+ 1506,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 147,
+ 2031,
+ 911,
+ 2588,
+ 1506,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 148,
+ 2031,
+ 967,
+ 2588,
+ 1023,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 149,
+ 2031,
+ 1051,
+ 2588,
+ 1121,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 150,
+ 2031,
+ 1149,
+ 2588,
+ 1289,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 151,
+ 2031,
+ 1345,
+ 2031,
+ 1506,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ +Item(
+ 152,
+ 84,
+ 911,
+ 1348,
+ 1506,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 153,
+ 84,
+ 911,
+ 1348,
+ 1506,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 154,
+ 84,
+ 911,
+ 1348,
+ 1506,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 155,
+ 84,
+ 911,
+ 679,
+ 1506,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 156,
+ 735,
+ 911,
+ 1292,
+ 1506,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 157,
+ 735,
+ 967,
+ 1292,
+ 1023,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 158,
+ 735,
+ 1051,
+ 1292,
+ 1191,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 159,
+ 735,
+ 1219,
+ 1292,
+ 1359,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 160,
+ 735,
+ 1415,
+ 735,
+ 1506,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ +Item(
+ 161,
+ 0,
+ 1506,
+ 1440,
+ 1849,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 162,
+ 84,
+ 1548,
+ 1356,
+ 1821,
+ false,
+ false,
+ true
+ ) {
+ +Item(
+ 163,
+ 84,
+ 1548,
+ 1356,
+ 1821,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 164,
+ 140,
+ 1548,
+ 1300,
+ 1821,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 165,
+ 140,
+ 1604,
+ 927,
+ 1765,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 166,
+ 140,
+ 1604,
+ 865,
+ 1688,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 167,
+ 140,
+ 1702,
+ 469,
+ 1765,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 168,
+ 140,
+ 1702,
+ 203,
+ 1765,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 169,
+ 217,
+ 1706,
+ 469,
+ 1762,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 170,
+ 983,
+ 1580,
+ 1300,
+ 1790,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 171,
+ 1040,
+ 1615,
+ 1243,
+ 1755,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 172,
+ 1040,
+ 1654,
+ 1103,
+ 1717,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 173,
+ 1103,
+ 1650,
+ 1243,
+ 1720,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 174,
+ 1117,
+ 1650,
+ 1243,
+ 1720,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ +Item(
+ 175,
+ 42,
+ 2770,
+ 1398,
+ 2770,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(176, 0, 2812, 1440, 3036, false, false, false) {
+ +Item(
+ 177,
+ 0,
+ 2812,
+ 1440,
+ 3036,
+ false,
+ false,
+ true
+ ) {
+ +Item(
+ 178,
+ 0,
+ 2812,
+ 1440,
+ 3036,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 179,
+ 0,
+ 2812,
+ 1440,
+ 3036,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 180,
+ 0,
+ 2812,
+ 288,
+ 3036,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 181,
+ 0,
+ 2812,
+ 288,
+ 3036,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 182,
+ 46,
+ 2847,
+ 242,
+ 2931,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 183,
+ 102,
+ 2847,
+ 186,
+ 2931,
+ false,
+ true,
+ false
+ )
+ }
+ +Item(
+ 184,
+ 14,
+ 2931,
+ 274,
+ 3001,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 185,
+ 74,
+ 2945,
+ 214,
+ 3001,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ +Item(
+ 186,
+ 288,
+ 2812,
+ 576,
+ 3036,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 187,
+ 288,
+ 2812,
+ 576,
+ 3036,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 188,
+ 334,
+ 2847,
+ 530,
+ 2931,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 189,
+ 390,
+ 2847,
+ 474,
+ 2931,
+ false,
+ true,
+ false
+ )
+ }
+ +Item(
+ 190,
+ 302,
+ 2931,
+ 562,
+ 3001,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 191,
+ 381,
+ 2945,
+ 484,
+ 3001,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ +Item(
+ 192,
+ 576,
+ 2812,
+ 864,
+ 3036,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 193,
+ 576,
+ 2812,
+ 864,
+ 3036,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 194,
+ 622,
+ 2847,
+ 818,
+ 2931,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 195,
+ 678,
+ 2847,
+ 762,
+ 2931,
+ false,
+ true,
+ false
+ )
+ }
+ +Item(
+ 196,
+ 590,
+ 2931,
+ 850,
+ 3001,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 197,
+ 649,
+ 2945,
+ 792,
+ 3001,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ +Item(
+ 198,
+ 864,
+ 2812,
+ 1152,
+ 3036,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 199,
+ 864,
+ 2812,
+ 1152,
+ 3036,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 200,
+ 910,
+ 2847,
+ 1106,
+ 2931,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 201,
+ 966,
+ 2847,
+ 1050,
+ 2931,
+ false,
+ true,
+ false
+ )
+ }
+ +Item(
+ 202,
+ 878,
+ 2931,
+ 1138,
+ 3001,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 203,
+ 964,
+ 2945,
+ 1052,
+ 3001,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ +Item(
+ 204,
+ 1152,
+ 2812,
+ 1440,
+ 3036,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 205,
+ 1152,
+ 2812,
+ 1440,
+ 3036,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 206,
+ 1198,
+ 2847,
+ 1394,
+ 2931,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 207,
+ 1254,
+ 2847,
+ 1338,
+ 2931,
+ false,
+ true,
+ false
+ )
+ }
+ +Item(
+ 208,
+ 1166,
+ 2931,
+ 1426,
+ 3001,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 209,
+ 1235,
+ 2945,
+ 1358,
+ 3001,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ +Item(210, 0, 0, 1440, 3120, false, false, false) {
+ +Item(211, 102, 2847, 186, 2931, false, false, false) {
+ +Item(
+ 212,
+ 102,
+ 2847,
+ 186,
+ 2931,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(213, 390, 2847, 474, 2931, false, false, false) {
+ +Item(
+ 214,
+ 390,
+ 2847,
+ 474,
+ 2931,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(215, 678, 2847, 762, 2931, false, false, false) {
+ +Item(
+ 216,
+ 678,
+ 2847,
+ 762,
+ 2931,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(217, 966, 2847, 1050, 2931, false, false, false) {
+ +Item(
+ 218,
+ 966,
+ 2847,
+ 1050,
+ 2931,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 219,
+ 1254,
+ 2847,
+ 1338,
+ 2931,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 220,
+ 1254,
+ 2847,
+ 1338,
+ 2931,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(221, 0, 0, 1, 1, false, false, false) {
+ +Item(222, 0, 0, 1440, 196, false, false, false)
+ }
+ }
+ }
+ +Item(223, 0, 0, 1440, 3120, false, false, false) {
+ +Item(224, 636, 1476, 804, 1644, false, false, false)
+ }
+ +Item(225, 0, 0, 1, 1, false, false, false)
+ }
+ +Item(226, 0, 0, 1440, 145, false, false, false) {
+ +Item(227, 0, 145, 1440, 145, false, false, false)
+ }
+ }
+ }
+ +Item(228, 0, 0, 1, 1, false, false, false)
+ }
+ }
+ }
+ +Item(229, 0, 3036, 1440, 3120, false, false, false)
+ +Item(230, 0, 0, 1440, 145, false, false, false)
+ }
+
+val exampleLayoutRects: Array<IntArray> = run {
+ val emptyIntArray = IntArray(0)
+ val results = Array(231) { emptyIntArray }
+
+ fun push(item: Item) {
+ results[item.id] = item.bounds
+ item.children.forEach { child -> push(child) }
+ }
+ push(rootItem)
+ for (bounds in results) {
+ assert(bounds !== emptyIntArray)
+ }
+ results
+}
+
+val scrollableItems: List<Item> = run {
+ val results = mutableListOf<Item>()
+
+ fun traverse(item: Item) {
+ if (item.scrollable) {
+ results.add(item)
+ }
+ item.children.forEach { child -> traverse(child) }
+ }
+ traverse(rootItem)
+ results
+}
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 ed3de3a..b6415a3 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
@@ -61,6 +61,7 @@
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.WindowInfo
+import androidx.compose.ui.spatial.RectManager
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.input.TextInputService
@@ -3299,6 +3300,8 @@
override val windowInfo: WindowInfo
get() = TODO("Not yet implemented")
+ override val rectManager: RectManager = RectManager()
+
@Deprecated(
"fontLoader is deprecated, use fontFamilyResolver",
replaceWith = ReplaceWith("fontFamilyResolver")
@@ -3415,6 +3418,9 @@
override fun transform(matrix: Matrix) {}
+ override val underlyingMatrix: Matrix
+ get() = Matrix()
+
override fun inverseTransform(matrix: Matrix) {}
override fun mapOffset(point: Offset, inverse: Boolean) = point
@@ -3427,6 +3433,8 @@
layoutChangeCount++
}
+ override fun onLayoutNodeDeactivated(layoutNode: LayoutNode) {}
+
override fun onInteropViewLayoutChange(view: InteropView) {}
override fun getFocusDirection(keyEvent: KeyEvent): FocusDirection? {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
index 461d659..0c03bf2 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
@@ -56,6 +56,7 @@
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.WindowInfo
+import androidx.compose.ui.spatial.RectManager
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.input.TextInputService
@@ -2907,6 +2908,8 @@
override val windowInfo: WindowInfo
get() = TODO("Not yet implemented")
+ override val rectManager: RectManager = RectManager()
+
@Deprecated(
"fontLoader is deprecated, use fontFamilyResolver",
replaceWith = ReplaceWith("fontFamilyResolver")
@@ -2989,6 +2992,8 @@
override fun onLayoutChange(layoutNode: LayoutNode) {}
+ override fun onLayoutNodeDeactivated(layoutNode: LayoutNode) {}
+
override fun onInteropViewLayoutChange(view: InteropView) {}
override fun getFocusDirection(keyEvent: KeyEvent): FocusDirection? {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt
index be78319..8370dfd 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt
@@ -51,6 +51,7 @@
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.WindowInfo
+import androidx.compose.ui.spatial.RectManager
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.input.TextInputService
@@ -149,6 +150,8 @@
override fun onLayoutChange(layoutNode: LayoutNode) {}
+ override fun onLayoutNodeDeactivated(layoutNode: LayoutNode) {}
+
override fun onInteropViewLayoutChange(view: InteropView) {}
@OptIn(InternalCoreApi::class) override var showLayoutBounds: Boolean = false
@@ -216,6 +219,8 @@
override val windowInfo: WindowInfo
get() = TODO("Not yet implemented")
+ override val rectManager: RectManager = RectManager()
+
@Deprecated(
"fontLoader is deprecated, use fontFamilyResolver",
replaceWith = ReplaceWith("fontFamilyResolver")
@@ -620,6 +625,9 @@
override fun transform(matrix: Matrix) {}
+ override val underlyingMatrix: Matrix
+ get() = Matrix()
+
override fun inverseTransform(matrix: Matrix) {}
override fun mapOffset(point: Offset, inverse: Boolean) = point
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/PlacementScopeMotionFrameOfReferenceTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/PlacementScopeMotionFrameOfReferenceTest.kt
new file mode 100644
index 0000000..6f6e935
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/PlacementScopeMotionFrameOfReferenceTest.kt
@@ -0,0 +1,293 @@
+/*
+ * 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.layout
+
+import androidx.activity.ComponentActivity
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.layout.requiredWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyGridState
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState
+import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
+import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.PageSize
+import androidx.compose.foundation.pager.PagerState
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.round
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import kotlin.test.assertEquals
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class PlacementScopeMotionFrameOfReferenceTest {
+ @get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
+
+ @Test
+ fun testLazyList() {
+ var offset by mutableStateOf(IntOffset(0, 0))
+ val coords = arrayOfNulls<LayoutCoordinates>(30)
+ var rootCoords: LayoutCoordinates? = null
+ val state = LazyListState()
+ val offsets =
+ listOf(
+ IntOffset(0, 0),
+ IntOffset(5, 20),
+ IntOffset(25, 0),
+ IntOffset(100, 10),
+ )
+ rule.setContent {
+ CompositionLocalProvider(LocalDensity provides Density(1f)) {
+ Box(
+ Modifier.layout { m, c ->
+ m.measure(c).run {
+ layout(width, height) {
+ rootCoords = coordinates
+ place(0, 0)
+ }
+ }
+ }
+ .offset { offset }
+ ) {
+ LazyColumn(state = state, modifier = Modifier.requiredHeight(100.dp)) {
+ items(30) { index ->
+ Box(Modifier.size(20.dp).onGloballyPositioned { coords[index] = it })
+ }
+ }
+ }
+ }
+ }
+ repeat(4) {
+ val itemId = it * 5
+ offset = offsets[it]
+ rule.runOnIdle { runBlocking { state.scrollToItem(itemId) } }
+ repeat(5) {
+ assertEquals(
+ offset,
+ coords[itemId + it]!!
+ .let {
+ rootCoords!!.localPositionOf(it, includeMotionFrameOfReference = false)
+ }
+ .round()
+ )
+ assertEquals(
+ offset + IntOffset(0, it * 20),
+ coords[itemId + it]!!
+ .let {
+ rootCoords!!.localPositionOf(it, includeMotionFrameOfReference = true)
+ }
+ .round()
+ )
+ }
+ }
+ }
+
+ @Test
+ fun testLazyGrid() {
+ var offset by mutableStateOf(IntOffset(0, 0))
+ val coords = arrayOfNulls<LayoutCoordinates>(60)
+ var rootCoords: LayoutCoordinates? = null
+ val state = LazyGridState()
+ val offsets =
+ listOf(
+ IntOffset(0, 0),
+ IntOffset(5, 20),
+ IntOffset(25, 0),
+ IntOffset(100, 10),
+ )
+ rule.setContent {
+ CompositionLocalProvider(LocalDensity provides Density(1f)) {
+ Box(
+ Modifier.layout { m, c ->
+ m.measure(c).run {
+ layout(width, height) {
+ rootCoords = coordinates
+ place(0, 0)
+ }
+ }
+ }
+ .offset { offset }
+ ) {
+ LazyVerticalGrid(
+ GridCells.Fixed(2),
+ modifier = Modifier.requiredHeight(100.dp).requiredWidth(40.dp),
+ state = state
+ ) {
+ items(60) { index ->
+ Box(Modifier.size(20.dp).onGloballyPositioned { coords[index] = it })
+ }
+ }
+ }
+ }
+ }
+ repeat(4) {
+ val itemId = it * 5 * 2
+ offset = offsets[it]
+ rule.runOnIdle { runBlocking { state.scrollToItem(itemId) } }
+ rule.waitForIdle()
+ repeat(5) {
+ assertEquals(
+ offset,
+ coords[itemId + it]!!
+ .let {
+ rootCoords!!.localPositionOf(it, includeMotionFrameOfReference = false)
+ }
+ .round()
+ )
+ assertEquals(
+ offset + IntOffset(0 + it % 2 * 20, it / 2 * 20),
+ coords[itemId + it]!!
+ .let {
+ rootCoords!!.localPositionOf(it, includeMotionFrameOfReference = true)
+ }
+ .round()
+ )
+ }
+ }
+ }
+
+ @Test
+ fun testLazyStaggeredGrid() {
+ var offset by mutableStateOf(IntOffset(0, 0))
+ val coords = arrayOfNulls<LayoutCoordinates>(60)
+ var rootCoords: LayoutCoordinates? = null
+ val state = LazyStaggeredGridState()
+ val offsets =
+ listOf(
+ IntOffset(0, 0),
+ IntOffset(5, 20),
+ IntOffset(25, 0),
+ IntOffset(100, 10),
+ )
+ rule.setContent {
+ CompositionLocalProvider(LocalDensity provides Density(1f)) {
+ Box(
+ Modifier.layout { m, c ->
+ m.measure(c).run {
+ layout(width, height) {
+ rootCoords = coordinates
+ place(0, 0)
+ }
+ }
+ }
+ .offset { offset }
+ ) {
+ LazyVerticalStaggeredGrid(
+ state = state,
+ columns = StaggeredGridCells.Fixed(2),
+ modifier = Modifier.requiredHeight(100.dp).requiredWidth(40.dp)
+ ) {
+ items(60) { index ->
+ Box(
+ Modifier.size(20.dp, ((index % 2) * 5).dp + 15.dp)
+ .onGloballyPositioned { coords[index] = it }
+ )
+ }
+ }
+ }
+ }
+ }
+ repeat(4) {
+ val itemId = it * 10
+ offset = offsets[it]
+ rule.runOnIdle { runBlocking { state.scrollToItem(itemId) } }
+ repeat(5) {
+ assertEquals(
+ offset,
+ coords[itemId + it]!!
+ .let {
+ rootCoords!!.localPositionOf(it, includeMotionFrameOfReference = false)
+ }
+ .round()
+ )
+ }
+ }
+ }
+
+ @Test
+ fun testPager() {
+ var offset by mutableStateOf(IntOffset(0, 0))
+ val coords = arrayOfNulls<LayoutCoordinates>(30)
+ var rootCoords: LayoutCoordinates? = null
+ val state = PagerState { 30 }
+ val offsets =
+ listOf(
+ IntOffset(0, 0),
+ IntOffset(5, 20),
+ IntOffset(25, 0),
+ IntOffset(100, 10),
+ )
+ rule.setContent {
+ CompositionLocalProvider(LocalDensity provides Density(1f)) {
+ Box(
+ Modifier.layout { m, c ->
+ m.measure(c).run {
+ layout(width, height) {
+ rootCoords = coordinates
+ place(0, 0)
+ }
+ }
+ }
+ .offset { offset }
+ ) {
+ HorizontalPager(
+ state,
+ pageSize = PageSize.Fixed(20.dp),
+ modifier = Modifier.requiredHeight(20.dp).requiredWidth(100.dp)
+ ) { index ->
+ Box(Modifier.size(20.dp, 20.dp).onGloballyPositioned { coords[index] = it })
+ }
+ }
+ }
+ }
+ repeat(4) {
+ val itemId = it * 5
+ offset = offsets[it]
+ rule.runOnIdle { runBlocking { state.scrollToPage(itemId) } }
+ repeat(5) {
+ assertEquals(
+ offset,
+ requireNotNull(coords[itemId + it]) { "item $itemId, it = $it" }
+ .let {
+ rootCoords!!.localPositionOf(it, includeMotionFrameOfReference = false)
+ }
+ .round(),
+ )
+ }
+ }
+ }
+}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/RectListIntegrationTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/RectListIntegrationTest.kt
new file mode 100644
index 0000000..2a326fe
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/RectListIntegrationTest.kt
@@ -0,0 +1,487 @@
+/*
+ * Copyright 2020 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.layout
+
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.platform.AndroidComposeView
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.scale
+import androidx.compose.ui.semantics.SemanticsActions.ScrollBy
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.hasTestTag
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SmallTest
+import junit.framework.TestCase.assertFalse
+import kotlin.math.ceil
+import kotlin.math.floor
+import kotlin.math.roundToInt
+import kotlin.test.assertTrue
+import org.junit.ComparisonFailure
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class RectListIntegrationTest {
+
+ @get:Rule val rule = createComposeRule()
+
+ @Test
+ @SmallTest
+ fun testSingleBox() {
+ rule.setContent { Box(Modifier.testTag("foo").size(10.dp)) }
+
+ rule.onNodeWithTag("foo").assertRectDp(0.dp, 0.dp, 10.dp, 10.dp)
+ }
+
+ @Test
+ @SmallTest
+ fun testNestedBox() {
+ rule.setContent {
+ Box(Modifier.padding(10.dp)) { Box(Modifier.testTag("foo").size(10.dp)) }
+ }
+
+ rule.onNodeWithTag("foo").assertRectDp(10.dp, 10.dp, 20.dp, 20.dp)
+ }
+
+ @Test
+ @SmallTest
+ fun testUpdatePosition() {
+ var toggle by mutableStateOf(false)
+
+ rule.setContent {
+ Box(Modifier.padding(if (toggle) 50.dp else 10.dp)) {
+ Box(Modifier.testTag("foo").size(10.dp))
+ }
+ }
+
+ rule.onNodeWithTag("foo").assertRectDp(10.dp, 10.dp, 20.dp, 20.dp)
+
+ rule.runOnIdle { toggle = !toggle }
+
+ rule.onNodeWithTag("foo").assertRectDp(50.dp, 50.dp, 60.dp, 60.dp)
+ }
+
+ @Test
+ @SmallTest
+ fun testUpdateTextPadding() {
+ var padding by mutableIntStateOf(0)
+
+ rule.setContent {
+ Box(Modifier.background(Color.Yellow).size(200.dp)) {
+ Text("Row", Modifier.testTag("text").padding(padding.dp).size(40.dp))
+ }
+ }
+
+ rule.onNodeWithTag("text").assertRectDp(0.dp, 0.dp, 40.dp, 40.dp)
+
+ rule.runOnIdle { padding += 10 }
+
+ rule.onNodeWithTag("text").assertRectDp(0.dp, 0.dp, 60.dp, 60.dp)
+ }
+
+ @Test
+ @SmallTest
+ fun testPaddings() {
+ rule.setContent {
+ Box(Modifier.padding(20.dp)) {
+ Box(Modifier.padding(20.dp)) {
+ Box(Modifier.padding(60.dp)) { Box(Modifier.testTag("test").size(100.dp)) }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("test").assertRectDp(100.dp, 100.dp, 200.dp, 200.dp)
+ }
+
+ @Test
+ @SmallTest
+ fun testPaddingsTwo() {
+ rule.setContent {
+ Box(Modifier.padding(4.dp)) { Box(Modifier.testTag("test").padding(4.dp).size(4.dp)) }
+ }
+
+ rule.onNodeWithTag("test").assertRectDp(4.dp, 4.dp, 16.dp, 16.dp)
+ }
+
+ @Test
+ @SmallTest
+ fun testPaddingsThree() {
+ var padding by mutableIntStateOf(20)
+ rule.setContent {
+ Box(Modifier.padding(padding.dp)) {
+ Box(Modifier.padding(20.dp)) {
+ Box(Modifier.padding(60.dp)) { Box(Modifier.testTag("test").size(100.dp)) }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("test").assertRectDp(100.dp, 100.dp, 200.dp, 200.dp)
+ padding += 10
+ rule.onNodeWithTag("test").assertRectDp(110.dp, 110.dp, 210.dp, 210.dp)
+ }
+
+ @Test
+ @SmallTest
+ fun testPaddingsFour() {
+ var padding by mutableIntStateOf(20)
+ rule.setContent {
+ Box(Modifier.offset { IntOffset(padding.dp.roundToPx(), padding.dp.roundToPx()) }) {
+ Box(Modifier.padding(20.dp)) {
+ Box(Modifier.padding(60.dp)) { Box(Modifier.testTag("test").size(100.dp)) }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("test").assertRectDp(100.dp, 100.dp, 200.dp, 200.dp)
+ padding += 10
+ rule.onNodeWithTag("test").assertRectDp(110.dp, 110.dp, 210.dp, 210.dp)
+ }
+
+ @Test
+ @SmallTest
+ fun testUpdateSize() {
+ var toggle by mutableStateOf(false)
+
+ rule.setContent { Box { Box(Modifier.testTag("foo").size(if (toggle) 50.dp else 10.dp)) } }
+
+ rule.onNodeWithTag("foo").assertRectDp(0.dp, 0.dp, 10.dp, 10.dp)
+
+ rule.runOnIdle { toggle = !toggle }
+
+ rule.onNodeWithTag("foo").assertRectDp(0.dp, 0.dp, 50.dp, 50.dp)
+ }
+
+ @Test
+ @SmallTest
+ fun testUpdateTranslation() {
+ var toggle by mutableStateOf(false)
+
+ rule.setContent {
+ Box(
+ Modifier.offset {
+ if (toggle) IntOffset(10.dp.roundToPx(), 10.dp.roundToPx()) else IntOffset.Zero
+ }
+ ) {
+ Box(Modifier.testTag("foo").size(10.dp))
+ }
+ }
+
+ rule.onNodeWithTag("foo").assertRectDp(0.dp, 0.dp, 10.dp, 10.dp)
+
+ rule.runOnIdle { toggle = !toggle }
+
+ rule.onNodeWithTag("foo").assertRectDp(10.dp, 10.dp, 20.dp, 20.dp)
+ }
+
+ @Test
+ @SmallTest
+ fun testRemovingNodeRemovesRect() {
+ var toggle by mutableStateOf(false)
+
+ rule.setContent {
+ if (!toggle) {
+ Box(Modifier.testTag("foo").size(10.dp))
+ }
+ }
+
+ val node = rule.onNodeWithTag("foo")
+
+ node.assertRectDp(0.dp, 0.dp, 10.dp, 10.dp)
+
+ val semanticsNode = node.fetchSemanticsNode()
+ val owner = semanticsNode.layoutNode.owner as? AndroidComposeView
+ val rectList = owner?.rectManager?.rects ?: error("Could not find rect list")
+
+ val nodeId = semanticsNode.id
+
+ rule.runOnIdle {
+ assertTrue(nodeId in rectList)
+ toggle = !toggle
+ }
+
+ rule.runOnIdle { assertFalse(nodeId in rectList) }
+ }
+
+ @Test
+ @SmallTest
+ fun testScrolling() {
+ rule.setContent {
+ val scrollState = rememberScrollState()
+ Column(Modifier.testTag("scroller").verticalScroll(scrollState)) {
+ for (i in 0 until 4) {
+ Box(Modifier.testTag("foo$i").width(200.dp).height(400.dp)) {
+ Box(Modifier.size(10.dp))
+ Box(Modifier.size(10.dp))
+ Box(Modifier.size(10.dp))
+ Box(Modifier.size(10.dp))
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("foo0").assertRectDp(0.dp, 0.dp, 200.dp, 400.dp)
+
+ rule.onNodeWithTag("foo3").assertRectDp(0.dp, 1200.dp, 200.dp, 1600.dp)
+
+ val scrollBy =
+ rule.onNodeWithTag("scroller").fetchSemanticsNode().config[ScrollBy].action
+ ?: error("No scrollByAction found")
+
+ val scrollDistance = with(rule.density) { 100.dp.toPx() }
+
+ scrollBy(0f, scrollDistance)
+
+ rule.onNodeWithTag("foo0").assertRectDp(0.dp, -100.dp, 200.dp, 300.dp)
+
+ rule.onNodeWithTag("foo3").assertRectDp(0.dp, 1100.dp, 200.dp, 1500.dp)
+ }
+
+ @Composable
+ fun ColorStripe(red: Int, green: Int, blue: Int) {
+ Canvas(Modifier.size(45.dp, 500.dp)) {
+ drawRect(Color(red = red, green = green, blue = blue))
+ }
+ }
+
+ @Test
+ @SmallTest
+ fun testScrollingWeirdness() {
+ rule.setContent {
+ val scrollState = rememberScrollState()
+ Column(Modifier.verticalScroll(scrollState)) {
+ Box(Modifier.testTag("foo").size(100.dp))
+ Box(Modifier.testTag("bar").size(100.dp))
+ }
+ }
+
+ rule.onNodeWithTag("foo").assertRectDp(0.dp, 0.dp, 100.dp, 100.dp)
+
+ rule.onNodeWithTag("bar").assertRectDp(0.dp, 100.dp, 100.dp, 200.dp)
+ }
+
+ @Test
+ @SmallTest
+ fun testRotatedBox() {
+ rule.setContent {
+ Box(
+ Modifier.testTag("outer").graphicsLayer {
+ translationX = 100.dp.toPx()
+ rotationZ = 45f
+ }
+ ) {
+ Box(Modifier.testTag("inner").size(100.dp))
+ }
+ }
+
+ rule.onNodeWithTag("outer").assertRectDp(0.dp, 0.dp, 100.dp, 100.dp)
+ rule.onNodeWithTag("inner").assertRectDp(79.dp, -21.dp, 220.dp, 121.dp)
+ }
+
+ @Test
+ @SmallTest
+ fun testScaledBox() {
+ var toggle by mutableStateOf(true)
+ rule.setContent {
+ Box(
+ Modifier.testTag("outer").padding(10.dp).graphicsLayer {
+ translationX = if (toggle) 0f else 100.dp.toPx()
+ rotationZ = if (toggle) 0f else 45f
+ }
+ ) {
+ Box(Modifier.testTag("inner").size(10.dp))
+ }
+ }
+
+ rule.onNodeWithTag("outer").assertRectDp(0.dp, 0.dp, 30.dp, 30.dp)
+ rule.onNodeWithTag("inner").assertRectDp(10.dp, 10.dp, 20.dp, 20.dp)
+ toggle = !toggle
+ rule.onNodeWithTag("inner").assertRectDp(108.dp, 8.dp, 122.dp, 22.dp)
+ }
+
+ @Test
+ @SmallTest
+ fun testScaledBoxUpdate() {
+ rule.setContent {
+ Box(Modifier.testTag("outer").padding(10.dp).scale(2f)) {
+ Box(Modifier.testTag("inner").size(10.dp))
+ }
+ }
+
+ rule.onNodeWithTag("outer").assertRectDp(0.dp, 0.dp, 40.dp, 40.dp)
+ rule.onNodeWithTag("inner").assertRectDp(5.dp, 5.dp, 25.dp, 25.dp)
+ }
+
+ @Test
+ @SmallTest
+ fun testScrollingNestedLayout() {
+ rule.setContent {
+ val scrollState = rememberScrollState()
+ Column(Modifier.testTag("scroller").verticalScroll(scrollState)) {
+ for (i in 0 until 8) {
+ Box(
+ Modifier.background(if (i % 2 == 0) Color.Yellow else Color.LightGray)
+ .size(200.dp)
+ ) {
+ Text("Row $i", Modifier.testTag("text$i").width(100.dp).height(20.dp))
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("text0").assertRectDp(0.dp, 0.dp, 100.dp, 20.dp)
+
+ rule.onNodeWithTag("text1").assertRectDp(0.dp, 200.dp, 100.dp, 220.dp)
+
+ rule.onNodeWithTag("text2").assertRectDp(0.dp, 400.dp, 100.dp, 420.dp)
+
+ val scrollBy =
+ rule.onNodeWithTag("scroller").fetchSemanticsNode().config[ScrollBy].action
+ ?: error("No scrollByAction found")
+
+ val scrollDistance = with(rule.density) { 100.dp.toPx() }
+
+ scrollBy(0f, scrollDistance)
+
+ rule.onNodeWithTag("text2").assertRectDp(0.dp, 300.dp, 100.dp, 320.dp)
+ }
+
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ @SmallTest
+ fun testLazyColumn() {
+ rule.setContent {
+ LazyColumn(Modifier.testTag("lazy")) {
+ items(200) { Box(Modifier.testTag("foo$it").height(100.dp).width(100.dp)) }
+ }
+ }
+
+ rule.waitUntilExactlyOneExists(hasTestTag("foo1"))
+ rule.waitUntilDoesNotExist(hasTestTag("foo20"))
+
+ rule.onNodeWithTag("foo0").assertRectDp(0.dp, 0.dp, 100.dp, 100.dp)
+ rule.onNodeWithTag("foo1").assertRectDp(0.dp, 100.dp, 100.dp, 200.dp)
+
+ val scrollBy =
+ rule.onNodeWithTag("lazy").fetchSemanticsNode().config[ScrollBy].action
+ ?: error("No scrollByAction found")
+
+ val scrollDistance = with(rule.density) { 2000.dp.toPx() }
+
+ scrollBy(0f, scrollDistance)
+
+ rule.waitUntilDoesNotExist(hasTestTag("foo0"))
+ rule.waitUntilExactlyOneExists(hasTestTag("foo20"))
+
+ rule.onNodeWithTag("foo20").assertRectTopWithinRange(-4.dp, 4.dp)
+ }
+
+ internal fun SemanticsNodeInteraction.assertRectDp(
+ left: Dp,
+ top: Dp,
+ right: Dp,
+ bottom: Dp,
+ ) = withRect { l, t, r, b ->
+ if (
+ !approxEquals(left, l) ||
+ !approxEquals(top, t) ||
+ !approxEquals(right, r) ||
+ !approxEquals(bottom, b)
+ ) {
+ val actualL = convertToDp(l)
+ val actualT = convertToDp(t)
+ val actualR = convertToDp(r)
+ val actualB = convertToDp(b)
+
+ val expectDpString = "[$left, $top, $right, $bottom]"
+ val actualDpString = "[$actualL, $actualT, $actualR, $actualB]"
+
+ throw ComparisonFailure(
+ "expected <$expectDpString> but was: <$actualDpString>",
+ expectDpString,
+ actualDpString
+ )
+ }
+ }
+
+ internal fun SemanticsNodeInteraction.assertRectTopWithinRange(
+ min: Dp,
+ max: Dp,
+ ) = withRect { _, t, _, _ ->
+ val topDp = convertToDp(t)
+
+ if (topDp < min || topDp > max) {
+ error("top was $topDp but was expected to be between [$min, $max]")
+ }
+ }
+
+ inline internal fun SemanticsNodeInteraction.withRect(
+ crossinline block: Density.(l: Int, t: Int, r: Int, b: Int) -> Unit
+ ) {
+ val node = fetchSemanticsNode()
+ val owner = node.layoutNode.owner as? AndroidComposeView
+ val rectList = owner?.rectManager?.rects ?: error("Could not find rect list")
+
+ with(rule.density) {
+ val found = rectList.withRect(node.id) { l, t, r, b -> block(l, t, r, b) }
+
+ if (!found) {
+ error("Node with ${node.id} not found in rectlist")
+ }
+ }
+ }
+
+ private fun Density.convertToDp(px: Int): Dp {
+ return (px / density).roundToInt().dp
+ }
+
+ private fun Density.approxEquals(dp: Dp, px: Int): Boolean {
+ val lower = floor((dp.value - 1f) * density).toInt()
+ val upper = ceil((dp.value + 1f) * density).toInt()
+ return px in lower..upper
+ }
+ // TODO: assert on number of times insert/update/move called
+}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitChildrenTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitChildrenTest.kt
index 2273b6a..3ea0bed 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitChildrenTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitChildrenTest.kt
@@ -19,6 +19,7 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.zIndex
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
@@ -104,6 +105,52 @@
assertThat(visitedChildren).containsExactly(child1, child2, child3).inOrder()
}
+ @Test
+ fun visitChildrenInOtherLayoutNodesInDrawOrder_zIndex() {
+ // Arrange.
+ abstract class TrackedNode : Modifier.Node()
+ val (node, child1, child2, child3) = List(5) { object : TrackedNode() {} }
+ val visitedChildren = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ Box(Modifier.elementOf(node)) {
+ Box(Modifier.elementOf(child1).zIndex(10f))
+ Box(Modifier.elementOf(child2).zIndex(-10f))
+ Box(Modifier.elementOf(child3))
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ node.visitChildren(Nodes.Any, zOrder = true) {
+ @Suppress("KotlinConstantConditions") if (it is TrackedNode) visitedChildren.add(it)
+ }
+ }
+
+ // Assert.
+ assertThat(visitedChildren).containsExactly(child2, child3, child1).inOrder()
+ }
+
+ @Test
+ fun visitChildrenInOtherLayoutNodesInDrawOrder_subcompose() {
+ // Arrange.
+ val (node, child1, child2, child3) = List(5) { object : Modifier.Node() {} }
+ val visitedChildren = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ ReverseMeasureLayout(
+ Modifier.elementOf(node),
+ { Box(Modifier.elementOf(child1)) },
+ { Box(Modifier.elementOf(child2)) },
+ { Box(Modifier.elementOf(child3)) }
+ )
+ }
+
+ // Act.
+ rule.runOnIdle { node.visitChildren(Nodes.Any, zOrder = true) { visitedChildren.add(it) } }
+
+ // Assert.
+ assertThat(visitedChildren).containsExactly(child1, child2, child3).inOrder()
+ }
+
@Ignore("b/278765590")
@Test
fun skipsUnattachedLocalChild() {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeIfTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeIfTest.kt
index 63f82d9..6f1e610 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeIfTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeIfTest.kt
@@ -19,6 +19,7 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.zIndex
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
@@ -134,6 +135,58 @@
.inOrder()
}
+ @Test
+ fun visitsItemsAcrossLayoutNodesInDrawOrder_zIndex() {
+ // Arrange.
+ abstract class TrackedNode : Modifier.Node()
+ val (node, child1, child2, child3) = List(5) { object : TrackedNode() {} }
+ val visitedChildren = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ Box(Modifier.elementOf(node)) {
+ Box(Modifier.elementOf(child1).zIndex(10f))
+ Box(Modifier.elementOf(child2).zIndex(-10f))
+ Box(Modifier.elementOf(child3))
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ node.visitSubtreeIf(Nodes.Any, zOrder = true) {
+ @Suppress("KotlinConstantConditions") if (it is TrackedNode) visitedChildren.add(it)
+ true
+ }
+ }
+
+ // Assert.
+ assertThat(visitedChildren).containsExactly(child2, child3, child1).inOrder()
+ }
+
+ @Test
+ fun visitsItemsAcrossLayoutNodesInDrawOrder_subcompose() {
+ // Arrange.
+ val (node, child1, child2, child3) = List(5) { object : Modifier.Node() {} }
+ val visitedChildren = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ ReverseMeasureLayout(
+ Modifier.elementOf(node),
+ { Box(Modifier.elementOf(child1)) },
+ { Box(Modifier.elementOf(child2)) },
+ { Box(Modifier.elementOf(child3)) }
+ )
+ }
+
+ // Act.
+ rule.runOnIdle {
+ node.visitSubtreeIf(Nodes.Any, zOrder = true) {
+ visitedChildren.add(it)
+ true
+ }
+ }
+
+ // Assert.
+ assertThat(visitedChildren).containsExactly(child1, child2, child3).inOrder()
+ }
+
@Ignore("b/278765590")
@Test
fun skipsUnattachedItems() {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeTest.kt
index 59d8aed..c2b585b5 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeTest.kt
@@ -19,6 +19,7 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.zIndex
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
@@ -67,9 +68,6 @@
assertThat(visitedChildren).containsExactly(localChild1, localChild2).inOrder()
}
- // TODO(ralu): I feel that this order of visiting children is incorrect, and we should
- // visit children in the order of composition. So instead of a stack, we probably need
- // to use a queue to hold the intermediate nodes.
@Test
fun differentLayoutNodes() {
// Arrange.
@@ -79,10 +77,10 @@
val visitedChildren = mutableListOf<Modifier.Node>()
rule.setContent {
Box(Modifier.elementOf(node).elementOf(child1).elementOf(child2)) {
- Box(Modifier.elementOf(child5).elementOf(child6)) {
- Box(Modifier.elementOf(child7).elementOf(child8))
+ Box(Modifier.elementOf(child3).elementOf(child4)) {
+ Box(Modifier.elementOf(child5).elementOf(child6))
}
- Box { Box(Modifier.elementOf(child3).elementOf(child4)) }
+ Box { Box(Modifier.elementOf(child7).elementOf(child8)) }
}
}
@@ -95,6 +93,54 @@
.inOrder()
}
+ @Test
+ fun differentLayoutNodesInDrawOrder_zIndex() {
+ // Arrange.
+ abstract class TrackedNode : Modifier.Node()
+ val (node, child1, child2, child3, child4) = List(5) { object : TrackedNode() {} }
+ val visitedChildren = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ Box(Modifier.elementOf(node)) {
+ Box(Modifier.elementOf(child1))
+ Box(Modifier.elementOf(child2).zIndex(10f)) {
+ Box(Modifier.elementOf(child3).zIndex(-10f))
+ }
+ Box { Box(Modifier.elementOf(child4)) }
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ node.visitSubtree(Nodes.Any, zOrder = true) {
+ @Suppress("KotlinConstantConditions") if (it is TrackedNode) visitedChildren.add(it)
+ }
+ }
+
+ // Assert.
+ assertThat(visitedChildren).containsExactly(child1, child4, child2, child3).inOrder()
+ }
+
+ @Test
+ fun differentLayoutNodesInDrawOrder_subcompose() {
+ // Arrange.
+ val (node, child1, child2, child3, child4) = List(5) { object : Modifier.Node() {} }
+ val visitedChildren = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ ReverseMeasureLayout(
+ Modifier.elementOf(node),
+ { Box(Modifier.elementOf(child1)) },
+ { Box(Modifier.elementOf(child2)) { Box(Modifier.elementOf(child3)) } },
+ { Box { Box(Modifier.elementOf(child4)) } }
+ )
+ }
+
+ // Act.
+ rule.runOnIdle { node.visitSubtree(Nodes.Any, zOrder = true) { visitedChildren.add(it) } }
+
+ // Assert.
+ assertThat(visitedChildren).containsExactly(child1, child2, child3, child4).inOrder()
+ }
+
@Ignore("b/278765590")
@Test
fun skipsUnattached() {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NestedVectorStackTests.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NestedVectorStackTests.kt
deleted file mode 100644
index 74a3c0e..0000000
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NestedVectorStackTests.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright 2022 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.runtime.collection.mutableVectorOf
-import org.junit.Assert
-import org.junit.Test
-
-class NestedVectorStackTests {
-
- @Test
- fun testPushPopOrder() {
- val stack = NestedVectorStack<Int>()
- stack.push(mutableVectorOf(1, 2, 3))
- stack.push(mutableVectorOf(4, 5, 6))
- stack.push(mutableVectorOf())
- stack.push(mutableVectorOf(7))
- stack.push(mutableVectorOf(8, 9))
- val result = buildString {
- while (stack.isNotEmpty()) {
- append(stack.pop())
- }
- }
- Assert.assertEquals("987654321", result)
- }
-
- @Test
- fun testPopInBetweenPushes() {
- val stack = NestedVectorStack<Int>()
- stack.push(mutableVectorOf(1, 2, 3, 4))
- stack.pop()
- stack.push(mutableVectorOf(4, 5, 6))
- stack.pop()
- stack.pop()
- stack.push(mutableVectorOf())
- stack.push(mutableVectorOf(5, 6, 7))
- stack.push(mutableVectorOf(8, 9))
- val result = buildString {
- while (stack.isNotEmpty()) {
- append(stack.pop())
- }
- }
- Assert.assertEquals("987654321", result)
- }
-}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt
index 61fe1dd..705a09d 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt
@@ -47,6 +47,7 @@
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.WindowInfo
import androidx.compose.ui.platform.invertTo
+import androidx.compose.ui.spatial.RectManager
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.input.TextInputService
@@ -422,6 +423,8 @@
override val windowInfo: WindowInfo
get() = TODO("Not yet implemented")
+ override val rectManager: RectManager = RectManager()
+
override val fontFamilyResolver: FontFamily.Resolver
get() = TODO("Not yet implemented")
@@ -522,6 +525,8 @@
layoutChangeCount++
}
+ override fun onLayoutNodeDeactivated(layoutNode: LayoutNode) {}
+
@InternalComposeUiApi override fun onInteropViewLayoutChange(view: InteropView) {}
override fun getFocusDirection(keyEvent: KeyEvent): FocusDirection? {
@@ -565,6 +570,9 @@
matrix.timesAssign(transform)
}
+ override val underlyingMatrix: Matrix
+ get() = transform
+
override fun inverseTransform(matrix: Matrix) {
matrix.timesAssign(inverseTransform)
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeUtils.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeUtils.kt
index 29b9c0a..e77e8d6 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeUtils.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeUtils.kt
@@ -16,7 +16,10 @@
package androidx.compose.ui.node
+import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.platform.InspectorInfo
/**
@@ -38,3 +41,30 @@
name = "testNode"
}
}
+
+@Composable
+internal fun ReverseMeasureLayout(modifier: Modifier, vararg contents: @Composable () -> Unit) =
+ SubcomposeLayout(modifier) { constraints ->
+ var layoutWidth = constraints.minWidth
+ var layoutHeight = constraints.minHeight
+ val subcomposes = mutableListOf<List<Placeable>>()
+
+ // Measure in reverse order
+ contents.reversed().forEachIndexed { index, content ->
+ subcomposes.add(
+ 0,
+ subcompose(index, content).map {
+ it.measure(constraints).also { placeable ->
+ layoutWidth = maxOf(layoutWidth, placeable.width)
+ layoutHeight = maxOf(layoutHeight, placeable.height)
+ }
+ }
+ )
+ }
+
+ layout(layoutWidth, layoutHeight) {
+
+ // But place in direct order - it sets direct draw order
+ subcomposes.forEach { placeables -> placeables.forEach { it.place(0, 0) } }
+ }
+ }
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingListParityTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingListParityTest.kt
index 8d0b0ee..9ba98f3 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingListParityTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingListParityTest.kt
@@ -332,6 +332,90 @@
}
}
+ @Test
+ fun equalLists_withEqualFlings_shouldFinishAtTheSameItem_regularGestureOne() = runBlocking {
+ val state = LazyListState()
+
+ // starting with view
+ createActivity(state)
+ checkVisibility(composeView(), View.GONE)
+ checkVisibility(recyclerView(), View.VISIBLE)
+
+ regularGestureOne(R.id.view_list)
+ rule.waitForIdle()
+ recyclerView().awaitScrollIdle()
+
+ val childAtTheTopOfView = layoutManager?.findFirstVisibleItemPosition() ?: 0
+
+ // switch visibilities
+ rule.runOnUiThread {
+ rule.activity.findViewById<RecyclerView>(R.id.view_list).visibility = View.GONE
+ rule.activity.findViewById<ComposeView>(R.id.compose_view).visibility = View.VISIBLE
+ }
+
+ checkVisibility(composeView(), View.VISIBLE)
+ checkVisibility(recyclerView(), View.GONE)
+
+ assertTrue { isValidGesture(recyclerView().motionEvents.filterNotNull()) }
+
+ // Inject the same events in compose view
+ rule.runOnUiThread {
+ for (event in recyclerView().motionEvents) {
+ composeView().dispatchTouchEvent(event)
+ }
+ }
+
+ rule.runOnIdle {
+ val currentTopInCompose = state.firstVisibleItemIndex
+ val diff = (currentTopInCompose - childAtTheTopOfView).absoluteValue
+ val message =
+ "Compose=$currentTopInCompose View=$childAtTheTopOfView " + "Difference was=$diff"
+ assertTrue(message) { diff <= ItemDifferenceThreshold }
+ }
+ }
+
+ @Test
+ fun equalLists_withEqualFlings_shouldFinishAtTheSameItem_regularGestureTwo() = runBlocking {
+ val state = LazyListState()
+
+ // starting with view
+ createActivity(state)
+ checkVisibility(composeView(), View.GONE)
+ checkVisibility(recyclerView(), View.VISIBLE)
+
+ regularGestureTwo(R.id.view_list)
+ rule.waitForIdle()
+ recyclerView().awaitScrollIdle()
+
+ val childAtTheTopOfView = layoutManager?.findFirstVisibleItemPosition() ?: 0
+
+ // switch visibilities
+ rule.runOnUiThread {
+ rule.activity.findViewById<RecyclerView>(R.id.view_list).visibility = View.GONE
+ rule.activity.findViewById<ComposeView>(R.id.compose_view).visibility = View.VISIBLE
+ }
+
+ checkVisibility(composeView(), View.VISIBLE)
+ checkVisibility(recyclerView(), View.GONE)
+
+ assertTrue { isValidGesture(recyclerView().motionEvents.filterNotNull()) }
+
+ // Inject the same events in compose view
+ rule.runOnUiThread {
+ for (event in recyclerView().motionEvents) {
+ composeView().dispatchTouchEvent(event)
+ }
+ }
+
+ rule.runOnIdle {
+ val currentTopInCompose = state.firstVisibleItemIndex
+ val diff = (currentTopInCompose - childAtTheTopOfView).absoluteValue
+ val message =
+ "Compose=$currentTopInCompose View=$childAtTheTopOfView " + "Difference was=$diff"
+ assertTrue(message) { diff <= ItemDifferenceThreshold }
+ }
+ }
+
private fun createActivity(state: LazyListState) {
rule.activityRule.scenario.createActivityWithComposeContent(
R.layout.android_compose_lists_fling
@@ -385,7 +469,7 @@
@Composable
fun TestComposeList(state: LazyListState) {
LazyColumn(Modifier.fillMaxSize(), state = state) {
- items(1000) {
+ items(2000) {
Box(modifier = Modifier.fillMaxWidth().height(64.dp).background(Color.Black)) {
Text(text = it.toString(), color = Color.White)
}
@@ -394,7 +478,7 @@
}
private class ListAdapter : RecyclerView.Adapter<ListViewHolder>() {
- val items = (0 until 1000).map { it.toString() }
+ val items = (0 until 2000).map { it.toString() }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder {
return ListViewHolder(
@@ -451,4 +535,4 @@
}
}
-private const val ItemDifferenceThreshold = 3
+private const val ItemDifferenceThreshold = 1
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingParityTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingParityTest.kt
index 4fef305..2e76112 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingParityTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingParityTest.kt
@@ -24,10 +24,9 @@
import androidx.activity.ComponentActivity
import androidx.annotation.LayoutRes
import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.awaitEachGesture
-import androidx.compose.foundation.gestures.awaitFirstDown
-import androidx.compose.foundation.gestures.awaitTouchSlopOrCancellation
import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.gestures.draggable2D
+import androidx.compose.foundation.gestures.rememberDraggable2DState
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
@@ -38,14 +37,10 @@
import androidx.compose.ui.background
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.AwaitPointerEventScope
-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.changedToUpIgnoreConsumed
-import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.input.pointer.positionChangedIgnoreConsumed
import androidx.compose.ui.input.pointer.util.VelocityTrackerAddPointsFix
-import androidx.compose.ui.input.pointer.util.addPointerInputChange
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.ViewConfiguration
@@ -70,9 +65,7 @@
import com.google.errorprone.annotations.CanIgnoreReturnValue
import kotlin.math.absoluteValue
import kotlin.test.assertTrue
-import kotlinx.coroutines.coroutineScope
import org.junit.Before
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -272,7 +265,6 @@
}
@Test
- @Ignore("b/299092669")
fun equalDraggable_withEqualSwipes_shouldProduceSimilarVelocity_orthogonal() {
// Arrange
createActivity(true)
@@ -306,6 +298,70 @@
assertIsWithinTolerance(latestComposeVelocity.y, latestVelocityInViewY)
}
+ @Test
+ fun equalDraggable_withEqualSwipes_shouldProduceSimilarVelocity_regularSituationOne() {
+ // Arrange
+ createActivity()
+ checkVisibility(composeView, View.GONE)
+ checkVisibility(draggableView, View.VISIBLE)
+
+ // Act: Use system to send motion events and collect them.
+ regularGestureOne(R.id.draggable_view)
+
+ val latestVelocityInViewY = draggableView.latestVelocity.y
+
+ // switch visibility
+ rule.runOnUiThread {
+ composeView.visibility = View.VISIBLE
+ draggableView.visibility = View.GONE
+ }
+
+ checkVisibility(composeView, View.VISIBLE)
+ checkVisibility(draggableView, View.GONE)
+
+ assertTrue { isValidGesture(draggableView.motionEvents.filterNotNull()) }
+
+ // Inject the same events in compose view
+ for (event in draggableView.motionEvents) {
+ composeView.dispatchTouchEvent(event)
+ }
+
+ // assert
+ assertIsWithinTolerance(latestComposeVelocity.y, latestVelocityInViewY)
+ }
+
+ @Test
+ fun equalDraggable_withEqualSwipes_shouldProduceSimilarVelocity_regularSituationTwo() {
+ // Arrange
+ createActivity()
+ checkVisibility(composeView, View.GONE)
+ checkVisibility(draggableView, View.VISIBLE)
+
+ // Act: Use system to send motion events and collect them.
+ regularGestureTwo(R.id.draggable_view)
+
+ val latestVelocityInViewY = draggableView.latestVelocity.y
+
+ // switch visibility
+ rule.runOnUiThread {
+ composeView.visibility = View.VISIBLE
+ draggableView.visibility = View.GONE
+ }
+
+ checkVisibility(composeView, View.VISIBLE)
+ checkVisibility(draggableView, View.GONE)
+
+ assertTrue { isValidGesture(draggableView.motionEvents.filterNotNull()) }
+
+ // Inject the same events in compose view
+ for (event in draggableView.motionEvents) {
+ composeView.dispatchTouchEvent(event)
+ }
+
+ // assert
+ assertIsWithinTolerance(latestComposeVelocity.y, latestVelocityInViewY)
+ }
+
private fun createActivity(twoDimensional: Boolean = false) {
rule.activityRule.scenario.createActivityWithComposeContent(
R.layout.velocity_tracker_compose_vs_view
@@ -394,6 +450,24 @@
)
}
+internal fun regularGestureOne(id: Int) {
+ Espresso.onView(withId(id))
+ .perform(
+ espressoSwipe(
+ SwiperWithTime(100),
+ GeneralLocation.CENTER,
+ GeneralLocation.BOTTOM_CENTER
+ )
+ )
+}
+
+internal fun regularGestureTwo(id: Int) {
+ Espresso.onView(withId(id))
+ .perform(
+ espressoSwipe(SwiperWithTime(70), GeneralLocation.CENTER, GeneralLocation.TOP_CENTER)
+ )
+}
+
private fun espressoSwipe(
swiper: Swiper,
start: CoordinatesProvider,
@@ -418,7 +492,10 @@
.background(Color.Black)
.then(
if (twoDimensional) {
- Modifier.draggable2D(onDragStopped)
+ Modifier.draggable2D(
+ rememberDraggable2DState {},
+ onDragStopped = onDragStopped
+ )
} else {
Modifier.draggable(
rememberDraggableState(onDelta = {}),
@@ -431,32 +508,6 @@
}
}
-fun Modifier.draggable2D(onDragStopped: (Velocity) -> Unit) =
- this.pointerInput(Unit) {
- coroutineScope {
- awaitEachGesture {
- val tracker = androidx.compose.ui.input.pointer.util.VelocityTracker()
- val initialDown =
- awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
- tracker.addPointerInputChange(initialDown)
-
- awaitTouchSlopOrCancellation(initialDown.id) { change, _ ->
- tracker.addPointerInputChange(change)
- change.consume()
- }
-
- val lastEvent =
- awaitDragOrUp(initialDown.id) {
- tracker.addPointerInputChange(it)
- it.consume()
- it.positionChangedIgnoreConsumed()
- }
- lastEvent?.let { tracker.addPointerInputChange(it) }
- onDragStopped(tracker.calculateVelocity())
- }
- }
- }
-
private fun ActivityScenario<*>.createActivityWithComposeContent(
@LayoutRes layout: Int,
content: @Composable () -> Unit,
@@ -512,8 +563,8 @@
return down.size == 1 && move.isNotEmpty() && up.size == 1
}
-// 1% tolerance
-private const val VelocityDifferenceTolerance = 0.1f
+// 5% tolerance
+private const val VelocityDifferenceTolerance = 0.05f
/** Copied from androidx.test.espresso.action.Swipe */
internal data class SwiperWithTime(val gestureDurationMs: Int) : Swiper {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogTest.kt
index 5abbb34..5906b9f 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogTest.kt
@@ -15,8 +15,11 @@
*/
package androidx.compose.ui.window
-import android.content.res.Configuration
+import android.util.DisplayMetrics
import android.view.KeyEvent
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_UP
+import android.view.View
import androidx.activity.OnBackPressedDispatcher
import androidx.activity.compose.BackHandler
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
@@ -35,8 +38,11 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.lerp
+import androidx.compose.ui.gesture.MotionEvent
+import androidx.compose.ui.gesture.PointerProperties
+import androidx.compose.ui.input.pointer.PointerCoords
import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.assertIsDisplayed
@@ -259,9 +265,9 @@
fun canFillScreenWidth_dependingOnProperty() {
var box1Width = 0
var box2Width = 0
- lateinit var configuration: Configuration
+ lateinit var displayMetrics: DisplayMetrics
rule.setContent {
- configuration = LocalConfiguration.current
+ displayMetrics = LocalView.current.context.resources.displayMetrics
Dialog(
onDismissRequest = {},
properties = DialogProperties(usePlatformDefaultWidth = false)
@@ -272,7 +278,7 @@
Box(Modifier.fillMaxSize().onSizeChanged { box2Width = it.width })
}
}
- val expectedWidth = with(rule.density) { configuration.screenWidthDp.dp.roundToPx() }
+ val expectedWidth = with(rule.density) { displayMetrics.widthPixels }
assertThat(box1Width).isEqualTo(expectedWidth)
assertThat(box2Width).isLessThan(box1Width)
}
@@ -313,6 +319,75 @@
}
}
+ @Test
+ fun dismissWhenClickingOutsideContent() {
+ var dismissed = false
+ var clicked = false
+ lateinit var composeView: View
+ val clickBoxTag = "clickBox"
+ rule.setContent {
+ Dialog(
+ onDismissRequest = { dismissed = true },
+ properties =
+ DialogProperties(
+ usePlatformDefaultWidth = false,
+ decorFitsSystemWindows = false
+ )
+ ) {
+ composeView = LocalView.current
+ Box(Modifier.size(10.dp).testTag(clickBoxTag).clickable { clicked = true })
+ }
+ }
+
+ // click inside the compose view
+ rule.onNodeWithTag(clickBoxTag).performClick()
+
+ rule.waitForIdle()
+
+ assertThat(dismissed).isFalse()
+ assertThat(clicked).isTrue()
+
+ clicked = false
+
+ // click outside the compose view
+ rule.waitForIdle()
+ var root = composeView
+ while (root.parent is View) {
+ root = root.parent as View
+ }
+
+ rule.runOnIdle {
+ val x = root.width / 4f
+ val y = root.height / 4f
+ val down =
+ MotionEvent(
+ eventTime = 0,
+ action = ACTION_DOWN,
+ numPointers = 1,
+ actionIndex = 0,
+ pointerProperties = arrayOf(PointerProperties(0)),
+ pointerCoords = arrayOf(PointerCoords(x, y)),
+ root
+ )
+ root.dispatchTouchEvent(down)
+ val up =
+ MotionEvent(
+ eventTime = 10,
+ action = ACTION_UP,
+ numPointers = 1,
+ actionIndex = 0,
+ pointerProperties = arrayOf(PointerProperties(0)),
+ pointerCoords = arrayOf(PointerCoords(x, y)),
+ root
+ )
+ root.dispatchTouchEvent(up)
+ }
+ rule.waitForIdle()
+
+ assertThat(dismissed).isTrue()
+ assertThat(clicked).isFalse()
+ }
+
private fun setupDialogTest(
closeDialogOnDismiss: Boolean = true,
dialogProperties: DialogProperties = DialogProperties(),
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogWithInsetsTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogWithInsetsTest.kt
index e85b14c..cde57e5 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogWithInsetsTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogWithInsetsTest.kt
@@ -15,12 +15,16 @@
*/
package androidx.compose.ui.window
+import android.content.res.Configuration
+import android.os.Build
import android.view.View
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.safeDrawing
+import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.material.TextField
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.Alignment
@@ -29,10 +33,18 @@
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.platform.SoftwareKeyboardController
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.requestFocus
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.core.graphics.Insets
@@ -41,8 +53,10 @@
import androidx.core.view.WindowInsetsControllerCompat
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
+import kotlin.math.roundToInt
import org.junit.Assert.assertNotEquals
import org.junit.Rule
import org.junit.Test
@@ -129,6 +143,90 @@
assertNotEquals(Insets.NONE, imeInsets)
}
+ @Test
+ fun dialogCanTakeEntireScreen() {
+ var size = IntSize.Zero
+ var displayWidth = 0
+ var displayHeight = 0
+ var insetsLeft = 0
+ var insetsTop = 0
+ var insetsRight = 0
+ var insetsBottom = 0
+ var textTop = 0
+ var controller: SoftwareKeyboardController? = null
+ rule.setContent {
+ val displayMetrics = LocalView.current.resources.displayMetrics
+ controller = LocalSoftwareKeyboardController.current
+ displayWidth = displayMetrics.widthPixels
+ displayHeight = displayMetrics.heightPixels
+ Box(Modifier.fillMaxSize()) {
+ Dialog(
+ {},
+ properties =
+ DialogProperties(
+ decorFitsSystemWindows = false,
+ usePlatformDefaultWidth = false
+ )
+ ) {
+ val insets = WindowInsets.safeDrawing
+
+ Box(
+ Modifier.fillMaxSize()
+ .layout { m, c ->
+ val p = m.measure(c)
+ size = IntSize(p.width, p.height)
+ insetsTop = insets.getTop(this)
+ insetsLeft = insets.getLeft(this, layoutDirection)
+ insetsBottom = insets.getBottom(this)
+ insetsRight = insets.getRight(this, layoutDirection)
+ layout(p.width, p.height) { p.place(0, 0) }
+ }
+ .safeDrawingPadding()
+ ) {
+ TextField(
+ value = "Hello",
+ onValueChange = {},
+ Modifier.align(Alignment.BottomStart).testTag("textField").onPlaced {
+ layoutCoordinates ->
+ textTop = layoutCoordinates.positionInRoot().y.roundToInt()
+ }
+ )
+ }
+ }
+ }
+ }
+ rule.waitForIdle()
+
+ if (
+ Build.VERSION.SDK_INT >= 35 &&
+ rule.activity.applicationContext.applicationInfo.targetSdkVersion >= 35
+ ) {
+ // On SDK >= 35, the metrics is the size of the entire screen
+ assertThat(size.width).isEqualTo(displayWidth)
+ assertThat(size.height).isEqualTo(displayHeight)
+ } else {
+ // On SDK < 35, the metrics is the size of the screen with some insets removed
+ assertThat(size.width).isAtLeast(displayWidth)
+ assertThat(size.height).isAtLeast(displayHeight)
+ }
+ // There is going to be some insets
+ assertThat(maxOf(insetsLeft, insetsTop, insetsRight, insetsBottom)).isNotEqualTo(0)
+
+ val hardKeyboardHidden =
+ rule.runOnUiThread { rule.activity.resources.configuration.hardKeyboardHidden }
+ if (hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO) {
+ return // can't launch the IME when the hardware keyboard is up.
+ }
+ val bottomInsetsBeforeIme = insetsBottom
+ val textTopBeforeIme = textTop
+ rule.onNodeWithTag("textField").requestFocus()
+ rule.waitUntil {
+ controller?.show()
+ insetsBottom != bottomInsetsBeforeIme
+ }
+ rule.runOnIdle { assertThat(textTop).isLessThan(textTopBeforeIme) }
+ }
+
private fun findDialogWindowProviderInParent(view: View): DialogWindowProvider? {
if (view is DialogWindowProvider) {
return view
diff --git a/compose/ui/ui/src/androidMain/baseline-prof.txt b/compose/ui/ui/src/androidMain/baseline-prof.txt
index e95c5d6..2c9eebe 100644
--- a/compose/ui/ui/src/androidMain/baseline-prof.txt
+++ b/compose/ui/ui/src/androidMain/baseline-prof.txt
@@ -49,6 +49,10 @@
# graphics include everything
HSPLandroidx/compose/ui/graphics/**->**(**)**
+#
+# spatial indexing include everything
+HSPLandroidx/compose/ui/spatial/**->**(**)**
+
# input
HSPLandroidx/compose/ui/input/InputMode;->**(**)**
HSPLandroidx/compose/ui/input/InputModeManagerImpl;->**(**)**
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/UiApplier.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/UiApplier.android.kt
index ef4b85e..2a1655b 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/UiApplier.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/UiApplier.android.kt
@@ -45,4 +45,8 @@
super.onEndChanges()
root.owner?.onEndApplyChanges()
}
+
+ override fun reuse() {
+ current.onReuse()
+ }
}
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 e64198f..259b52c 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
@@ -72,6 +72,8 @@
import androidx.compose.runtime.referentialEqualityPolicy
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.ui.ComposeUiFlags
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.SessionMutex
@@ -179,6 +181,7 @@
import androidx.compose.ui.semantics.EmptySemanticsModifier
import androidx.compose.ui.semantics.SemanticsOwner
import androidx.compose.ui.semantics.findClosestParentNode
+import androidx.compose.ui.spatial.RectManager
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.createFontFamilyResolver
@@ -1010,6 +1013,10 @@
override fun onDetach(node: LayoutNode) {
measureAndLayoutDelegate.onNodeDetached(node)
requestClearInvalidObservations()
+ @OptIn(ExperimentalComposeUiApi::class)
+ if (ComposeUiFlags.isRectTrackingEnabled) {
+ rectManager.remove(node)
+ }
}
fun requestClearInvalidObservations() {
@@ -1039,6 +1046,10 @@
// to the front of the list, so removing in a chunk is cheaper than removing one-by-one
endApplyChangesListeners.removeRange(0, size)
}
+ @OptIn(ExperimentalComposeUiApi::class)
+ if (ComposeUiFlags.isRectTrackingEnabled) {
+ rectManager.dispatchCallbacks()
+ }
}
override fun registerOnEndApplyChangesListener(listener: () -> Unit) {
@@ -1312,6 +1323,10 @@
measureAndLayoutDelegate.dispatchOnPositionedCallbacks()
dispatchPendingInteropLayoutCallbacks()
}
+ @OptIn(ExperimentalComposeUiApi::class)
+ if (ComposeUiFlags.isRectTrackingEnabled) {
+ rectManager.dispatchCallbacks()
+ }
}
}
@@ -1462,7 +1477,12 @@
measureAndLayoutDelegate.dispatchOnPositionedCallbacks(forceDispatch = positionChanged)
}
- override fun onDraw(canvas: android.graphics.Canvas) {}
+ override fun onDraw(canvas: android.graphics.Canvas) {
+ @OptIn(ExperimentalComposeUiApi::class)
+ if (ComposeUiFlags.isRectTrackingEnabled) {
+ rectManager.dispatchCallbacks()
+ }
+ }
override fun createLayer(
drawBlock: (canvas: Canvas, parentLayer: GraphicsLayer?) -> Unit,
@@ -1562,6 +1582,15 @@
}
}
+ override val rectManager = RectManager()
+
+ override fun onLayoutNodeDeactivated(layoutNode: LayoutNode) {
+ @OptIn(ExperimentalComposeUiApi::class)
+ if (ComposeUiFlags.isRectTrackingEnabled) {
+ rectManager.remove(layoutNode)
+ }
+ }
+
override fun onInteropViewLayoutChange(view: InteropView) {
isPendingInteropViewLayoutChangeDispatch = true
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/GraphicsLayerOwnerLayer.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/GraphicsLayerOwnerLayer.android.kt
index 60f3cf9..51bdd81 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/GraphicsLayerOwnerLayer.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/GraphicsLayerOwnerLayer.android.kt
@@ -403,6 +403,9 @@
return matrixCache
}
+ override val underlyingMatrix: Matrix
+ get() = getMatrix()
+
private fun getInverseMatrix(): Matrix? {
val inverseMatrix = inverseMatrixCache ?: Matrix().also { inverseMatrixCache = it }
if (!isInverseMatrixDirty) {
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt
index 2d34ae9..5c56366 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt
@@ -338,6 +338,9 @@
ownerView.recycle(this)
}
+ override val underlyingMatrix: Matrix
+ get() = matrixCache.calculateMatrix(renderNode)
+
override fun mapOffset(point: Offset, inverse: Boolean): Offset {
return if (inverse) {
matrixCache.mapInverse(renderNode, point)
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt
index a92d4f4..127641a 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt
@@ -77,6 +77,9 @@
private val matrixCache = LayerMatrixCache(getMatrix)
+ override val underlyingMatrix: Matrix
+ get() = matrixCache.calculateMatrix(this)
+
/**
* Local copy of the transform origin as GraphicsLayerModifier can be implemented as a model
* object. Update this field within [updateLayerProperties] and use it in [resize] or other
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/spatial/RectListDebugger.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/spatial/RectListDebugger.android.kt
new file mode 100644
index 0000000..a5bb81c
--- /dev/null
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/spatial/RectListDebugger.android.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.spatial
+
+import android.annotation.SuppressLint
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Paint
+import androidx.compose.ui.graphics.PaintingStyle
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
+import androidx.compose.ui.graphics.nativeCanvas
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasurePolicy
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.node.DrawModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.invalidateDraw
+import androidx.compose.ui.node.requireOwner
+import androidx.compose.ui.unit.Constraints
+
+@Composable
+internal fun RectListDebugger(modifier: Modifier = Modifier) {
+ Layout(modifier.then(RectListDebuggerModifierElement), EmptyFillMeasurePolicy)
+}
+
+private object EmptyFillMeasurePolicy : MeasurePolicy {
+ override fun MeasureScope.measure(
+ measurables: List<Measurable>,
+ constraints: Constraints
+ ): MeasureResult {
+ return layout(constraints.maxWidth, constraints.maxHeight) {}
+ }
+}
+
+@SuppressLint("ModifierNodeInspectableProperties")
+private object RectListDebuggerModifierElement :
+ ModifierNodeElement<RectListDebuggerModifierNode>() {
+ override fun create() = RectListDebuggerModifierNode()
+
+ override fun hashCode() = 123
+
+ override fun equals(other: Any?) = other === this
+
+ override fun update(node: RectListDebuggerModifierNode) {}
+}
+
+private class RectListDebuggerModifierNode : DrawModifierNode, Modifier.Node() {
+ private var paint =
+ Paint()
+ .also {
+ it.color = Color.Red
+ it.style = PaintingStyle.Stroke
+ }
+ .asFrameworkPaint()
+
+ var token: Any? = null
+
+ override fun onAttach() {
+ token = requireOwner().rectManager.registerOnChangedCallback { invalidateDraw() }
+ }
+
+ override fun onDetach() {
+ requireOwner().rectManager.unregisterOnChangedCallback(token)
+ }
+
+ override fun ContentDrawScope.draw() {
+ val rectList = requireOwner().rectManager.rects
+ val canvas = drawContext.canvas.nativeCanvas
+ val paint = paint
+ rectList.forEachRect { _, l, t, r, b ->
+ canvas.drawRect(l.toFloat(), t.toFloat(), r.toFloat(), b.toFloat(), paint)
+ }
+ }
+}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
index 83c30cd..475f5dba 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
@@ -20,13 +20,16 @@
import android.graphics.Outline
import android.os.Build
import android.view.ContextThemeWrapper
+import android.view.Gravity
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
+import android.view.View.OnLayoutChangeListener
import android.view.ViewGroup
import android.view.ViewOutlineProvider
import android.view.Window
import android.view.WindowManager
+import android.widget.FrameLayout
import androidx.activity.ComponentDialog
import androidx.activity.addCallback
import androidx.compose.runtime.Composable
@@ -57,8 +60,11 @@
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastMap
import androidx.compose.ui.util.fastMaxBy
-import androidx.compose.ui.util.fastRoundToInt
+import androidx.core.view.OnApplyWindowInsetsListener
+import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsAnimationCompat
+import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.findViewTreeViewModelStoreOwner
import androidx.lifecycle.setViewTreeLifecycleOwner
@@ -77,16 +83,19 @@
* @property securePolicy Policy for setting [WindowManager.LayoutParams.FLAG_SECURE] on the
* dialog's window.
* @property usePlatformDefaultWidth Whether the width of the dialog's content should be limited to
- * the platform default, which is smaller than the screen width.
+ * the platform default, which is smaller than the screen width. It is recommended to use
+ * [decorFitsSystemWindows] set to `false` when [usePlatformDefaultWidth] is false to support
+ * using the entire screen and avoiding UI glitches on some devices when the IME animates in.
* @property decorFitsSystemWindows Sets [WindowCompat.setDecorFitsSystemWindows] value. Set to
* `false` to use WindowInsets. If `false`, the
* [soft input mode][WindowManager.LayoutParams.softInputMode] will be changed to
* [WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE] and `android:windowIsFloating` is set to
- * `false` for Android [R][Build.VERSION_CODES.R] and earlier.
+ * `false` when [decorFitsSystemWindows] is false. When
+ * `targetSdk` >= [Build.VERSION_CODES.VANILLA_ICE_CREAM], [decorFitsSystemWindows] can only be
+ * `false` and this property doesn't have any effect.
*/
@Immutable
-actual class DialogProperties
-constructor(
+actual class DialogProperties(
actual val dismissOnBackPress: Boolean = true,
actual val dismissOnClickOutside: Boolean = true,
val securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit,
@@ -218,6 +227,7 @@
private var content: @Composable () -> Unit by mutableStateOf({})
var usePlatformDefaultWidth = false
+ var decorFitsSystemWindows = false
override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
private set
@@ -229,50 +239,16 @@
createComposition()
}
- override fun internalOnMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
- if (usePlatformDefaultWidth) {
- super.internalOnMeasure(widthMeasureSpec, heightMeasureSpec)
- } else {
- // usePlatformDefaultWidth false, so don't want to limit the dialog width to the Android
- // platform default. Therefore, we create a new measure spec for width, which
- // corresponds to the full screen width. We do the same for height, even if
- // ViewRootImpl gives it to us from the first measure.
- val displayWidthMeasureSpec =
- MeasureSpec.makeMeasureSpec(displayWidth, MeasureSpec.AT_MOST)
- val displayHeightMeasureSpec =
- MeasureSpec.makeMeasureSpec(displayHeight, MeasureSpec.AT_MOST)
- super.internalOnMeasure(displayWidthMeasureSpec, displayHeightMeasureSpec)
- }
- }
-
- override fun internalOnLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
- super.internalOnLayout(changed, left, top, right, bottom)
- // Now set the content size as fixed layout params, such that ViewRootImpl knows
- // the exact window size.
- if (!usePlatformDefaultWidth) {
- val child = getChildAt(0) ?: return
- window.setLayout(child.measuredWidth, child.measuredHeight)
- }
- }
-
- private val displayWidth: Int
- get() {
- val density = context.resources.displayMetrics.density
- return (context.resources.configuration.screenWidthDp * density).fastRoundToInt()
- }
-
- private val displayHeight: Int
- get() {
- val density = context.resources.displayMetrics.density
- return (context.resources.configuration.screenHeightDp * density).fastRoundToInt()
- }
-
@Composable
override fun Content() {
content()
}
}
+private fun adjustedDecorFitsSystemWindows(dialogProperties: DialogProperties, context: Context) =
+ dialogProperties.decorFitsSystemWindows &&
+ context.applicationInfo.targetSdkVersion < Build.VERSION_CODES.VANILLA_ICE_CREAM
+
private class DialogWrapper(
private var onDismissRequest: () -> Unit,
private var properties: DialogProperties,
@@ -288,16 +264,16 @@
*/
ContextThemeWrapper(
composeView.context,
- if (
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.S || properties.decorFitsSystemWindows
- ) {
+ if (adjustedDecorFitsSystemWindows(properties, composeView.context)) {
R.style.DialogWindowTheme
} else {
R.style.FloatingDialogWindowTheme
}
)
),
- ViewRootForInspector {
+ ViewRootForInspector,
+ OnApplyWindowInsetsListener,
+ OnLayoutChangeListener {
private val dialogLayout: DialogLayout
@@ -308,15 +284,12 @@
override val subCompositionView: AbstractComposeView
get() = dialogLayout
- private val defaultSoftInputMode: Int
-
init {
val window = window ?: error("Dialog has no window")
- defaultSoftInputMode =
- window.attributes.softInputMode and WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST
window.requestFeature(Window.FEATURE_NO_TITLE)
window.setBackgroundDrawableResource(android.R.color.transparent)
- WindowCompat.setDecorFitsSystemWindows(window, properties.decorFitsSystemWindows)
+ val decorFitsSystemWindows = adjustedDecorFitsSystemWindows(properties, context)
+ WindowCompat.setDecorFitsSystemWindows(window, decorFitsSystemWindows)
dialogLayout =
DialogLayout(context, window).apply {
// Set unique id for AbstractComposeView. This allows state restoration for the
@@ -336,10 +309,8 @@
override fun getOutline(view: View, result: Outline) {
result.setRect(0, 0, view.width, view.height)
// We set alpha to 0 to hide the view's shadow and let the composable to
- // draw
- // its own shadow. This still enables us to get the extra space needed
- // in the
- // surface.
+ // draw its own shadow. This still enables us to get the extra space
+ // needed in the surface.
result.alpha = 0f
}
}
@@ -359,7 +330,38 @@
// Turn of all clipping so shadows can be drawn outside the window
(window.decorView as? ViewGroup)?.disableClipping()
- setContentView(dialogLayout)
+ // Center the ComposeView in a FrameLayout
+ val frameLayout = FrameLayout(context)
+ frameLayout.addView(
+ dialogLayout,
+ FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.WRAP_CONTENT,
+ FrameLayout.LayoutParams.WRAP_CONTENT
+ )
+ .also { it.gravity = Gravity.CENTER }
+ )
+ frameLayout.setOnClickListener { onDismissRequest() }
+ ViewCompat.setOnApplyWindowInsetsListener(frameLayout, this)
+ ViewCompat.setWindowInsetsAnimationCallback(
+ frameLayout,
+ object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
+ override fun onProgress(
+ insets: WindowInsetsCompat,
+ runningAnimations: MutableList<WindowInsetsAnimationCompat>
+ ): WindowInsetsCompat {
+ return insets.inset(
+ dialogLayout.left,
+ dialogLayout.top,
+ frameLayout.width - dialogLayout.right,
+ frameLayout.height - dialogLayout.bottom
+ )
+ }
+ }
+ )
+ dialogLayout.addOnLayoutChangeListener(this)
+ frameLayout.addOnLayoutChangeListener(this)
+
+ setContentView(frameLayout)
dialogLayout.setViewTreeLifecycleOwner(composeView.findViewTreeLifecycleOwner())
dialogLayout.setViewTreeViewModelStoreOwner(composeView.findViewTreeViewModelStoreOwner())
dialogLayout.setViewTreeSavedStateRegistryOwner(
@@ -430,21 +432,42 @@
this.properties = properties
setSecurePolicy(properties.securePolicy)
setLayoutDirection(layoutDirection)
- if (properties.usePlatformDefaultWidth && !dialogLayout.usePlatformDefaultWidth) {
- // Undo fixed size in internalOnLayout, which would suppress size changes when
- // usePlatformDefaultWidth is true.
- window?.setLayout(
- WindowManager.LayoutParams.WRAP_CONTENT,
- WindowManager.LayoutParams.WRAP_CONTENT
- )
- }
dialogLayout.usePlatformDefaultWidth = properties.usePlatformDefaultWidth
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
- if (properties.decorFitsSystemWindows) {
- window?.setSoftInputMode(defaultSoftInputMode)
- } else {
- @Suppress("DEPRECATION")
- window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
+ val decorFitsSystemWindows = adjustedDecorFitsSystemWindows(properties, context)
+ dialogLayout.decorFitsSystemWindows = decorFitsSystemWindows
+ val window = window
+ if (window != null) {
+ val softInput =
+ when {
+ decorFitsSystemWindows ->
+ WindowManager.LayoutParams.SOFT_INPUT_ADJUST_UNSPECIFIED
+ Build.VERSION.SDK_INT < Build.VERSION_CODES.S ->
+ @Suppress("DEPRECATION") WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
+ else -> WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING
+ }
+ window.setSoftInputMode(softInput)
+ val attrs = window.attributes
+ val measurementWidth =
+ if (properties.usePlatformDefaultWidth) {
+ WindowManager.LayoutParams.WRAP_CONTENT
+ } else {
+ WindowManager.LayoutParams.MATCH_PARENT
+ }
+ val measurementHeight =
+ if (properties.usePlatformDefaultWidth || decorFitsSystemWindows) {
+ WindowManager.LayoutParams.WRAP_CONTENT
+ } else {
+ WindowManager.LayoutParams.MATCH_PARENT
+ }
+ if (
+ attrs.width != measurementWidth ||
+ attrs.height != measurementHeight ||
+ attrs.gravity != Gravity.CENTER
+ ) {
+ attrs.width = measurementWidth
+ attrs.height = measurementHeight
+ attrs.gravity = Gravity.CENTER
+ window.attributes = attrs
}
}
}
@@ -466,6 +489,28 @@
// Prevents the dialog from dismissing itself
return
}
+
+ override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
+ val left = dialogLayout.left
+ val top = dialogLayout.top
+ val right = v.width - dialogLayout.right
+ val bottom = v.height - dialogLayout.bottom
+ return insets.inset(left, top, right, bottom)
+ }
+
+ override fun onLayoutChange(
+ v: View,
+ left: Int,
+ top: Int,
+ right: Int,
+ bottom: Int,
+ oldLeft: Int,
+ oldTop: Int,
+ oldRight: Int,
+ oldBottom: Int
+ ) {
+ v.requestApplyInsets()
+ }
}
@Composable
diff --git a/compose/ui/ui/src/androidMain/res/values/styles.xml b/compose/ui/ui/src/androidMain/res/values/styles.xml
index e1211d4..d0e837b 100644
--- a/compose/ui/ui/src/androidMain/res/values/styles.xml
+++ b/compose/ui/ui/src/androidMain/res/values/styles.xml
@@ -19,11 +19,13 @@
<style name="DialogWindowTheme">
<item name="android:windowClipToOutline">false</item>
</style>
- <!-- Style for decorFitsSystemWindows = false on API 30 and earlier. WindowInsets won't
- be set on Dialogs without android:windowIsFloating set to false. -->
+ <!-- Style for decorFitsSystemWindows = false -->
<style name="FloatingDialogWindowTheme">
<item name="android:windowClipToOutline">false</item>
<item name="android:dialogTheme">@style/FloatingDialogTheme</item>
+ <item name="android:statusBarColor">@android:color/transparent</item>
+ <item name="android:navigationBarColor">@android:color/transparent</item>
+ <item name="android:backgroundDimEnabled">true</item>
</style>
<style name="FloatingDialogTheme">
<item name="android:windowIsFloating">false</item>
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
index c1363c7..0328f65 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
@@ -209,14 +209,14 @@
layout(d)
}
val recorder = Recorder()
- x.visitSubtree(Nodes.Draw, recorder)
+ x.visitSubtree(Nodes.Draw, block = recorder)
assertThat(recorder.recorded)
.isEqualTo(
listOf(
a.wrapped,
b,
- d,
c.wrapped,
+ d,
)
)
}
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
index 48999dc..09e3b36 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
@@ -67,6 +67,7 @@
import androidx.compose.ui.semantics.SemanticsConfiguration
import androidx.compose.ui.semantics.SemanticsModifier
import androidx.compose.ui.semantics.SemanticsPropertyReceiver
+import androidx.compose.ui.spatial.RectManager
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.input.TextInputService
@@ -2367,6 +2368,8 @@
override val windowInfo: WindowInfo
get() = TODO("Not yet implemented")
+ override val rectManager: RectManager = RectManager()
+
@Deprecated(
"fontLoader is deprecated, use fontFamilyResolver",
replaceWith = ReplaceWith("fontFamilyResolver")
@@ -2508,6 +2511,9 @@
matrix.timesAssign(transform)
}
+ override val underlyingMatrix: Matrix
+ get() = transform
+
override fun inverseTransform(matrix: Matrix) {
matrix.timesAssign(inverseTransform)
}
@@ -2526,6 +2532,8 @@
layoutChangeCount++
}
+ override fun onLayoutNodeDeactivated(layoutNode: LayoutNode) {}
+
@InternalComposeUiApi override fun onInteropViewLayoutChange(view: InteropView) {}
override fun getFocusDirection(keyEvent: KeyEvent): FocusDirection? {
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
index bea163c..c4fce35 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
@@ -49,6 +49,7 @@
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.WindowInfo
+import androidx.compose.ui.spatial.RectManager
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.input.TextInputService
@@ -376,6 +377,8 @@
override val windowInfo: WindowInfo
get() = TODO("Not yet implemented")
+ override val rectManager: RectManager = RectManager()
+
@Deprecated(
"fontLoader is deprecated, use fontFamilyResolver",
replaceWith = ReplaceWith("fontFamilyResolver")
@@ -441,6 +444,8 @@
override fun onLayoutChange(layoutNode: LayoutNode) = TODO("Not yet implemented")
+ override fun onLayoutNodeDeactivated(layoutNode: LayoutNode) {}
+
override fun onInteropViewLayoutChange(view: InteropView) = TODO("Not yet implemented")
override fun getFocusDirection(keyEvent: KeyEvent) = TODO("Not yet implemented")
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/NestedVectorStackTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/NestedVectorStackTest.kt
deleted file mode 100644
index 951cb3d..0000000
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/NestedVectorStackTest.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * 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.runtime.collection.mutableVectorOf
-import com.google.common.truth.Truth
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class NestedVectorStackTest {
-
- @Test
- fun testEnumerationOrder() {
- val stack = NestedVectorStack<Int>()
- stack.push(mutableVectorOf(1, 2, 3))
- stack.push(mutableVectorOf(4, 5, 6))
-
- Truth.assertThat(stack.enumerate()).isEqualTo(listOf(6, 5, 4, 3, 2, 1))
- }
-
- @Test
- fun testEnumerationOrderPartiallyPoppingMiddleVectors() {
- val stack = NestedVectorStack<Int>()
- stack.push(mutableVectorOf(1, 2, 3))
-
- Truth.assertThat(stack.pop()).isEqualTo(3)
-
- stack.push(mutableVectorOf(4, 5, 6))
-
- Truth.assertThat(stack.pop()).isEqualTo(6)
-
- Truth.assertThat(stack.enumerate()).isEqualTo(listOf(5, 4, 2, 1))
- }
-
- @Test
- fun testEnumerationOrderFullyPoppingMiddleVectors() {
- val stack = NestedVectorStack<Int>()
- stack.push(mutableVectorOf(1, 2, 3))
-
- Truth.assertThat(stack.pop()).isEqualTo(3)
- Truth.assertThat(stack.pop()).isEqualTo(2)
- Truth.assertThat(stack.pop()).isEqualTo(1)
-
- stack.push(mutableVectorOf(4, 5, 6))
-
- Truth.assertThat(stack.pop()).isEqualTo(6)
-
- Truth.assertThat(stack.enumerate()).isEqualTo(listOf(5, 4))
- }
-}
-
-internal fun <T> NestedVectorStack<T>.enumerate(): List<T> {
- val result = mutableListOf<T>()
- var item: T? = pop()
- while (item != null) {
- result.add(item)
- item = if (isNotEmpty()) pop() else null
- }
- return result
-}
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/spatial/RectListTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/spatial/RectListTest.kt
new file mode 100644
index 0000000..21050a3
--- /dev/null
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/spatial/RectListTest.kt
@@ -0,0 +1,764 @@
+/*
+ * Copyright 2020 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.spatial
+
+import androidx.collection.mutableIntListOf
+import androidx.compose.ui.util.fastForEach
+import kotlin.math.max
+import kotlin.math.min
+import kotlin.random.Random
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class RectListTest {
+
+ @Test
+ fun testInsert() {
+ val list = RectList()
+ list.insert(1, 1, 1, 2, 2)
+ assertIntersections(list, 1, 1, 2, 2, setOf(1))
+ }
+
+ @Test
+ fun testInsertsAndIntersections() {
+ val list = RectList()
+ // top left, 1x1 rect at 1,1
+ list.insert(1, 1, 1, 2, 2)
+ // top right, 1x1 rect at 11,1
+ list.insert(2, 11, 1, 12, 2)
+ // bottom left, 1x1 rect at 1,11
+ list.insert(3, 1, 11, 2, 12)
+ // bottom right, 1x1 rect at 11,11
+ list.insert(4, 11, 11, 12, 12)
+ // middle, 2,2 rect at 9,9
+ list.insert(5, 9, 9, 11, 11)
+ // top left, 1x1 rect at 5,5
+ list.insert(6, 5, 5, 6, 6)
+
+ // 1x1 rect at 3,3. nothing intersects.
+ assertIntersections(list, 3, 3, 4, 4, emptySet())
+
+ // top left
+ assertIntersections(list, 0, 0, 10, 10, setOf(1, 5, 6))
+
+ // top right
+ assertIntersections(list, 10, 0, 20, 10, setOf(5, 2))
+
+ // bottom left
+ assertIntersections(list, 0, 10, 10, 20, setOf(5, 3))
+
+ // bottom right
+ assertIntersections(list, 10, 10, 20, 20, setOf(5, 4))
+ }
+
+ @Test
+ fun testInsertExampleData() {
+ val list = RectList()
+ val testData = exampleLayoutRects
+ for (i in testData.indices) {
+ val rect = testData[i]
+ list.insert(
+ i,
+ rect[0],
+ rect[1],
+ rect[2],
+ rect[3],
+ )
+ }
+ }
+
+ @Test
+ fun testFindIntersectingPoint() {
+ val testData = exampleLayoutRects
+ val queries = pointerInputQueries
+
+ // first 17 rects are big enough that they cover all queries
+ val bigRects = listOf(0, 1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 210, 223)
+
+ val expectedResults =
+ arrayOf(
+ bigRects +
+ listOf(43, 44, 45, 46, 88, 106, 107, 161, 162, 163, 164, 170, 171, 173, 174),
+ bigRects + listOf(43, 44, 45, 46, 47, 48, 49, 55, 56),
+ bigRects + listOf(43, 44, 45, 46, 47, 48, 57, 58, 59, 65, 66, 67, 68),
+ bigRects + listOf(43, 44, 45, 46, 88, 89, 93, 94, 95, 96, 97, 98, 99, 104),
+ bigRects + listOf(16, 17, 18, 25, 26),
+ )
+
+ // we do a manual `rectContainsPoint` query here to validate that for all of the expected
+ // results, it returns true
+ for (i in expectedResults.indices) {
+ val x = queries[i][0]
+ val y = queries[i][1]
+ val results = expectedResults[i]
+ for (j in results.indices) {
+ val itemId = results[j]
+ val rect = exampleLayoutRects[itemId]
+ val (l, r, t, b) = rect
+ assert(
+ rectContainsPoint(
+ x,
+ y,
+ l,
+ r,
+ t,
+ b,
+ )
+ )
+ }
+ }
+
+ // populate the list
+ val rectList = RectList()
+ for (i in testData.indices) {
+ val rect = testData[i]
+ rectList.insert(
+ i,
+ rect[0],
+ rect[1],
+ rect[2],
+ rect[3],
+ )
+ }
+ // assert that forEachIntersection returns the expected results for each query
+ for (i in queries.indices) {
+ val list = mutableListOf<Int>()
+ val point = queries[i]
+ rectList.forEachIntersection(
+ point[0],
+ point[1],
+ ) {
+ list.add(it)
+ }
+ assertEquals(expectedResults[i].sorted(), list.sorted())
+ }
+ }
+
+ private fun insertRecursive(qt: RectList, item: Item, scrollableId: Int) {
+ val bounds = item.bounds
+
+ qt.insert(
+ item.id,
+ bounds[0],
+ bounds[1],
+ bounds[2],
+ bounds[3],
+ parentId = scrollableId,
+ )
+ item.children.fastForEach {
+ insertRecursive(qt, it, if (item.scrollable) item.id else scrollableId)
+ }
+ }
+
+ @Test
+ fun testUpdate() {
+ val testData = exampleLayoutRects
+ val list = RectList()
+ insertRecursive(list, rootItem, -1)
+ val bounds = testData[100]
+
+ assertValueHasRect(list, 100, bounds[0], bounds[1], bounds[2], bounds[3])
+
+ list.update(100, 1, 2, 3, 4)
+
+ assertValueHasRect(list, 100, 1, 2, 3, 4)
+ }
+
+ private fun assertValueHasRect(list: RectList, itemId: Int, l: Int, t: Int, r: Int, b: Int) {
+ var called = false
+ val expected = "[$l,$t,$r,$b]"
+ list.withRect(itemId) { w, x, y, z ->
+ called = true
+ assertEquals(expected, "[$w,$x,$y,$z]")
+ }
+ if (!called) {
+ error("RectList did not have an item with id $itemId")
+ }
+ }
+
+ @Test
+ fun testUpdateAllItems() {
+ val testData = exampleLayoutRects
+ val r = Random(1234)
+ val list = RectList()
+ insertRecursive(list, rootItem, -1)
+ for (i in testData.indices) {
+ val rect = testData[i]
+ val x = r.nextInt(-100, 100)
+ val y = r.nextInt(-100, 100)
+ list.update(
+ i,
+ min(max(rect[0] + x, 0), 1439),
+ min(max(rect[1] + y, 0), 3119),
+ min(max(rect[2] + x, 0), 1439),
+ min(max(rect[3] + y, 0), 3119),
+ )
+ }
+ }
+
+ @Test
+ fun testUpdateScrollableContainer() {
+ val scrollableItems = scrollableItems
+ val r = Random(1234)
+ val qt = RectList()
+ insertRecursive(qt, rootItem, -1)
+ scrollableItems.fastForEach {
+ val x = r.nextInt(-100, 100)
+ val y = r.nextInt(-100, 100)
+ val bounds = it.bounds
+ qt.update(
+ it.id,
+ max(bounds[0] + x, 0),
+ max(bounds[1] + y, 0),
+ max(bounds[2] + x, 0),
+ max(bounds[3] + y, 0),
+ )
+ }
+ }
+
+ @Test
+ fun testNearestNeighbor() {
+ val list = RectList()
+ for (x in 0 until 10) {
+ for (y in 0 until 10) {
+ val id = x * 10 + y
+ list.insert(id, 10 * x, 10 * y, 10 * x + 10, 10 * y + 10)
+ }
+ }
+
+ val expectedResults =
+ arrayOf(
+ // arrays of [x, y, score]
+ intArrayOf(4, 3, 1), // immediate to the right should definitely be the winner
+ intArrayOf(
+ 4,
+ 2,
+ 11
+ ), // "up one" should tie with "down one" but still be a lowish score
+ intArrayOf(
+ 4,
+ 4,
+ 11
+ ), // "up one" should tie with "down one" but still be a lowish score
+ // TODO: we can tweak the scoring algorithm to have a higher penalty for not
+ // overlapping, which might put this rectangle in 2nd place. The current focus algo
+ // seems to heavily prioritize "in beam" elements, which are ones that would have
+ // overlap, and might place this rectangle higher
+ intArrayOf(5, 3, 11), // two to the right should not win, but also have a low score.
+ )
+
+ var i = 0
+ // nearest neighbor to the right of
+ list.findKNearestNeighbors(AxisEast, 4, 30, 30, 40, 40) { score, id, _, _, _, _ ->
+ val x = id / 10
+ val y = id % 10
+ val expected = expectedResults[i]
+ assertEquals(expected[0], x)
+ assertEquals(expected[1], y)
+ assertEquals(expected[2], score)
+ i++
+ }
+ }
+
+ @Test
+ fun testFindNearestNeighborInDirection() {
+ val testData = exampleLayoutRects
+ val queries = nearestNeighborQueries
+ val numberOfResults = 4
+ val qt = RectList()
+ for (i in testData.indices) {
+ val rect = testData[i]
+ qt.insert(
+ i,
+ rect[0],
+ rect[1],
+ rect[2],
+ rect[3],
+ )
+ }
+ for (i in queries.indices) {
+ for (direction in 1..4) {
+ val list = mutableIntListOf()
+ val bounds = queries[i]
+ qt.findKNearestNeighbors(
+ direction,
+ numberOfResults,
+ bounds[0],
+ bounds[1],
+ bounds[2],
+ bounds[3],
+ ) { _, id, _, _, _, _ ->
+ list.add(id)
+ }
+ }
+ }
+ }
+
+ @Test
+ fun testRectanglePacking() {
+ val rect = packXY(1, 2)
+ assertEquals(1, unpackX(rect))
+ assertEquals(2, unpackY(rect))
+ }
+
+ @Test
+ fun testMaxValueRectanglePacking() {
+ val maxValue = Int.MAX_VALUE
+
+ val rect = packXY(maxValue, 0)
+ assertEquals(maxValue, unpackX(rect))
+ assertEquals(0, unpackY(rect))
+
+ val rect1 = packXY(0, maxValue)
+ assertEquals(0, unpackX(rect1))
+ assertEquals(maxValue, unpackY(rect1))
+
+ val rect2 = packXY(maxValue, maxValue)
+ assertEquals(maxValue, unpackX(rect2))
+ assertEquals(maxValue, unpackY(rect2))
+ }
+
+ @Test
+ fun testMinValueRectanglePacking() {
+ val minValue = Int.MIN_VALUE
+
+ val rect = packXY(minValue, 0)
+ assertEquals(minValue, unpackX(rect))
+ assertEquals(0, unpackY(rect))
+
+ val rect1 = packXY(0, minValue)
+ assertEquals(0, unpackX(rect1))
+ assertEquals(minValue, unpackY(rect1))
+
+ val rect2 = packXY(minValue, minValue)
+ assertEquals(minValue, unpackX(rect2))
+ assertEquals(minValue, unpackY(rect2))
+ }
+
+ @Test
+ fun testMetaPacking() {
+ val meta =
+ packMeta(
+ itemId = 1,
+ parentId = 2,
+ lastChildOffset = 3,
+ focusable = false,
+ gesturable = true
+ )
+ assertEquals(1, unpackMetaValue(meta))
+ assertEquals(2, unpackMetaParentId(meta))
+ assertEquals(3, unpackMetaLastChildOffset(meta))
+ assertEquals(0, unpackMetaFocusable(meta))
+ assertEquals(1, unpackMetaGesturable(meta))
+ }
+
+ @Test
+ fun testMetaPackingNegativeScrollableValue() {
+ val meta =
+ packMeta(
+ itemId = 10,
+ parentId = -1,
+ lastChildOffset = 0,
+ focusable = true,
+ gesturable = false,
+ )
+ assertEquals(10, unpackMetaValue(meta))
+ // TODO: this actually returns 268,435,455. Not sure if we need to change this or not.
+ // assertEquals(-1, unpackMetaParentScrollableValue(meta))
+ assertEquals(1, unpackMetaFocusable(meta))
+ assertEquals(0, unpackMetaGesturable(meta))
+ }
+
+ private fun rectIntersectsRect(src: Rect, l: Int, t: Int, r: Int, b: Int): Boolean {
+ return rectIntersectsRect(
+ packXY(src.l, src.t),
+ packXY(src.r, src.b),
+ packXY(l, t),
+ packXY(r, b),
+ )
+ }
+
+ private fun distanceScore(axis: Int, query: Rect, target: Rect): Int {
+ return distanceScore(
+ axis,
+ query.l,
+ query.t,
+ query.r,
+ query.b,
+ target.l,
+ target.t,
+ target.r,
+ target.b,
+ )
+ }
+
+ @Test
+ fun testRectIntersectsRect() {
+ val src = Rect(10, 10, 20, 20)
+
+ // Not overlapping or touching
+ // ====
+
+ // top left
+ assertFalse(rectIntersectsRect(src, 1, 1, 2, 2))
+
+ // top right
+ assertFalse(rectIntersectsRect(src, 24, 1, 25, 2))
+
+ // bottom left
+ assertFalse(rectIntersectsRect(src, 1, 23, 2, 24))
+
+ // bottom right
+ assertFalse(rectIntersectsRect(src, 24, 24, 25, 25))
+
+ // top
+ assertFalse(rectIntersectsRect(src, 15, 5, 16, 6))
+
+ // left
+ assertFalse(rectIntersectsRect(src, 5, 15, 6, 16))
+
+ // bottom
+ assertFalse(rectIntersectsRect(src, 15, 25, 16, 26))
+
+ // right
+ assertFalse(rectIntersectsRect(src, 25, 15, 26, 16))
+
+ // Touching but not Overlapping
+ // ====
+
+ // just touches top left corner
+ assertTrue(rectIntersectsRect(src, 1, 1, 10, 10))
+
+ // just touches top right corner
+ assertTrue(rectIntersectsRect(src, 20, 1, 30, 10))
+
+ // just touches bottom right corner
+ assertTrue(rectIntersectsRect(src, 20, 20, 30, 30))
+
+ // just touches bottom left corner
+ assertTrue(rectIntersectsRect(src, 1, 20, 10, 30))
+
+ // left side is touching but not overlapping
+ assertTrue(rectIntersectsRect(src, 1, 10, 10, 20))
+
+ // right side is touching but not overlapping
+ assertTrue(rectIntersectsRect(src, 20, 10, 30, 20))
+
+ // top side is touching but not overlapping
+ assertTrue(rectIntersectsRect(src, 10, 1, 20, 10))
+
+ // bottom side is touching but not overlapping
+ assertTrue(rectIntersectsRect(src, 10, 20, 20, 30))
+
+ // Clear Intersection
+ // ===
+
+ // partial overlap in top left corner
+ assertTrue(rectIntersectsRect(src, 1, 1, 11, 11))
+
+ // src is inside of dest
+ assertTrue(rectIntersectsRect(src, 1, 1, 30, 30))
+
+ // dest is inside of src
+ assertTrue(rectIntersectsRect(src, 15, 15, 16, 16))
+
+ // full exact overlap
+ assertTrue(rectIntersectsRect(src, 10, 10, 20, 20))
+
+ // Zero Area Rectangles
+ // ===
+
+ // destination is zero rect outside of src
+ assertFalse(rectIntersectsRect(src, 1, 1, 1, 1))
+
+ // destination is zero rect inside of src
+ assertTrue(rectIntersectsRect(src, 15, 15, 15, 15))
+
+ // destination is zero rect with height inside of src
+ assertTrue(rectIntersectsRect(src, 15, 15, 15, 16))
+
+ // destination is zero rect with width inside of src
+ assertTrue(rectIntersectsRect(src, 15, 15, 16, 15))
+
+ // src is zero rect outside of dest
+ assertFalse(
+ rectIntersectsRect(
+ Rect(1, 1, 1, 1),
+ 10,
+ 10,
+ 20,
+ 20,
+ )
+ )
+
+ // src is zero rect inside of dest
+ assertTrue(
+ rectIntersectsRect(
+ Rect(15, 15, 15, 15),
+ 10,
+ 10,
+ 20,
+ 20,
+ )
+ )
+
+ // src is zero rect with height inside of dest
+ assertTrue(
+ rectIntersectsRect(
+ Rect(15, 15, 15, 16),
+ 10,
+ 10,
+ 20,
+ 20,
+ )
+ )
+
+ // src is zero rect with width inside of dest
+ assertTrue(
+ rectIntersectsRect(
+ Rect(15, 15, 16, 15),
+ 10,
+ 10,
+ 20,
+ 20,
+ )
+ )
+ }
+
+ @Test
+ fun testDistanceScore() {
+ val queryRect = Rect(10, 10, 20, 20)
+
+ // Negative Distance (opposite axis)
+ // ===
+
+ // Any rectangle which overlaps with the query rectangle should be
+ // disallowed (negative value)
+ assertNegative(distanceScore(AxisEast, queryRect, Rect(11, 11, 19, 19)))
+
+ assertNegative(distanceScore(AxisSouth, queryRect, Rect(11, 11, 19, 19)))
+
+ assertNegative(distanceScore(AxisWest, queryRect, Rect(11, 11, 19, 19)))
+
+ assertNegative(distanceScore(AxisNorth, queryRect, Rect(11, 11, 19, 19)))
+
+ // Perfect Overlaps, edges touching
+ // ===
+
+ // "perfect" overlap to the east of this rect, we expect
+ // a low but positive score
+ assertEquals(distanceScore(AxisEast, queryRect, Rect(20, 10, 30, 20)), 1)
+
+ // "perfect" overlap to the south of this rect, we expect
+ // a low but positive score
+ assertEquals(distanceScore(AxisSouth, queryRect, Rect(10, 20, 20, 30)), 1)
+
+ // "perfect" overlap to the west of this rect, we expect
+ // a low but positive score
+ assertEquals(distanceScore(AxisWest, queryRect, Rect(0, 10, 10, 20)), 1)
+
+ // "perfect" overlap to the north of this rect, we expect
+ // a low but positive score
+ assertEquals(distanceScore(AxisNorth, queryRect, Rect(10, 0, 20, 10)), 1)
+
+ // 1,1 rectangle 2px away along axis. Should be positive, but smaller number
+ // ===
+
+ assertEquals(distanceScore(AxisEast, queryRect, Rect(22, 15, 23, 16)), 30)
+
+ assertEquals(distanceScore(AxisSouth, queryRect, Rect(15, 22, 16, 23)), 30)
+
+ assertEquals(distanceScore(AxisWest, queryRect, Rect(7, 15, 8, 16)), 30)
+
+ assertEquals(distanceScore(AxisNorth, queryRect, Rect(15, 7, 16, 8)), 30)
+
+ // 1,1 rectangle 10px away along axis. Should be positive, but larger number
+ // ===
+
+ assertEquals(distanceScore(AxisEast, queryRect, Rect(30, 15, 31, 16)), 110)
+
+ assertEquals(distanceScore(AxisSouth, queryRect, Rect(15, 30, 16, 31)), 110)
+
+ assertEquals(distanceScore(AxisWest, queryRect, Rect(0, 15, 1, 16)), 100)
+
+ assertEquals(distanceScore(AxisNorth, queryRect, Rect(15, 0, 16, 1)), 100)
+ }
+
+ @Test
+ fun testDefragment() {
+ val r = RectList()
+
+ val toRemove = listOf(2, 7, 8)
+
+ for (i in 0 until 10) {
+ r.insert(
+ i,
+ 1,
+ 1,
+ 2,
+ 2,
+ )
+ }
+
+ assertEquals(30, r.itemsSize)
+
+ for (i in toRemove) {
+ r.remove(i)
+ }
+
+ // itemsSize still won't change, since the removed items are just
+ // tombstoned at this point
+ assertEquals(30, r.itemsSize)
+
+ for (i in 0 until 10) {
+ if (i !in toRemove) {
+ assertRectWithIdEquals(r, i, 1, 1, 2, 2)
+ }
+ }
+
+ r.defragment()
+
+ assertEquals(21, r.itemsSize)
+
+ for (i in 0 until 10) {
+ if (i !in toRemove) {
+ assertRectWithIdEquals(r, i, 1, 1, 2, 2)
+ }
+ }
+ }
+
+ @Test
+ fun testUpdateScrollable2() {
+ val r = RectList()
+
+ // insert scrollable container
+ r.insert(
+ 1,
+ 10,
+ 10,
+ 20,
+ 20,
+ )
+
+ // insert child container
+ r.insert(
+ 2,
+ 10,
+ 10,
+ 20,
+ 20,
+ parentId = 1,
+ )
+
+ assertRectWithIdEquals(r, 2, 10, 10, 20, 20)
+
+ // move child items up by 1
+ r.updateSubhierarchy(
+ id = 1,
+ deltaX = 0,
+ deltaY = -1,
+ )
+
+ assertRectWithIdEquals(r, 2, 10, 9, 20, 19)
+
+ // move child items up by 10 more
+ r.updateSubhierarchy(
+ id = 1,
+ deltaX = 0,
+ deltaY = -10,
+ )
+
+ assertRectWithIdEquals(r, 2, 10, -1, 20, 9)
+ }
+
+ // TODO: test update scrollable behavior
+ // TODO: test point intersection
+
+ private data class Rect(val l: Int, val t: Int, val r: Int, val b: Int)
+}
+
+fun assertNegative(actual: Int) {
+ assert(actual < 0) { "Expected negative value, got $actual" }
+}
+
+internal fun assertIntersections(
+ grid: RectList,
+ l: Int,
+ t: Int,
+ r: Int,
+ b: Int,
+ expected: Set<Int>
+) {
+ val actualSet = mutableSetOf<Int>()
+ grid.forEachIntersection(l, t, r, b) {
+ assert(actualSet.add(it)) { "Encountered $it more than once" }
+ }
+ assertEquals(expected, actualSet)
+}
+
+internal fun rectContainsPoint(
+ x: Int,
+ y: Int,
+ l: Int,
+ t: Int,
+ r: Int,
+ b: Int,
+): Boolean {
+ return (l < x) and (x < r) and (t < y) and (y < b)
+}
+
+internal fun assertRectWithIdEquals(
+ rectList: RectList,
+ id: Int,
+ l: Int,
+ t: Int,
+ r: Int,
+ b: Int,
+) {
+ rectList.withRect(id) { w, x, y, z ->
+ assertRectEquals(
+ l,
+ t,
+ r,
+ b,
+ w,
+ x,
+ y,
+ z,
+ )
+ }
+}
+
+fun assertRectEquals(
+ l1: Int,
+ t1: Int,
+ r1: Int,
+ b1: Int,
+ l2: Int,
+ t2: Int,
+ r2: Int,
+ b2: Int,
+) {
+ assert(l1 == l2 && t1 == t2 && r1 == r2 && b1 == b2) {
+ "Expected: [$l1, $t1, $r1, $b1] Actual: [$l2, $t2, $r2, $b2]"
+ }
+}
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/spatial/SpatialTestData.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/spatial/SpatialTestData.kt
new file mode 100644
index 0000000..6283fde
--- /dev/null
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/spatial/SpatialTestData.kt
@@ -0,0 +1,2309 @@
+/*
+ * 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.spatial
+
+val occludingRectQueries =
+ arrayOf(
+ intArrayOf(490, 2073, 945, 2693),
+ intArrayOf(84, 2777, 1356, 2993),
+ intArrayOf(966, 2074, 1379, 2703),
+ intArrayOf(84, 1548, 1356, 1821),
+ intArrayOf(35, 2073, 490, 2693),
+ )
+
+val nearestNeighborQueries =
+ arrayOf(
+ intArrayOf(56, 2074, 469, 2703), // left side app image
+ intArrayOf(288, 2812, 576, 3036), // bottom nav bar middle button
+ intArrayOf(1048, 478, 1187, 548), // install button
+ intArrayOf(983, 1580, 1300, 1790), // show button
+ intArrayOf(1258, 1933, 1342, 2017), // more icon
+ )
+
+val pointerInputQueries =
+ arrayOf(
+ intArrayOf(1120, 1654), // Show button
+ intArrayOf(1263, 1943), // three dots button
+ intArrayOf(615, 2312), // app image
+ intArrayOf(1100, 496), // install button
+ intArrayOf(710, 215), // search bar
+ )
+
+class Item(
+ val id: Int,
+ val bounds: IntArray,
+ val scrollable: Boolean,
+ val focusable: Boolean,
+ val pointerInput: Boolean,
+) {
+ val children: MutableList<Item> = mutableListOf()
+
+ operator fun Item.unaryPlus() {
+ @Suppress("LABEL_RESOLVE_WILL_CHANGE") [email protected](this)
+ }
+}
+
+fun Item(
+ id: Int,
+ l: Int,
+ t: Int,
+ r: Int,
+ b: Int,
+ scrollable: Boolean,
+ focusable: Boolean,
+ pointerInput: Boolean,
+): Item = Item(id, intArrayOf(l, t, r, b), scrollable, focusable, pointerInput)
+
+fun Item(
+ id: Int,
+ l: Int,
+ t: Int,
+ r: Int,
+ b: Int,
+ scrollable: Boolean,
+ focusable: Boolean,
+ pointerInput: Boolean,
+ scope: Item.() -> Unit
+): Item {
+ return Item(id, intArrayOf(l, t, r, b), scrollable, focusable, pointerInput).apply { scope() }
+}
+
+val rootItem =
+ Item(0, 0, 0, 1440, 3120, false, false, false) {
+ +Item(1, 0, 0, 1440, 3120, false, false, false) {
+ +Item(2, 0, 0, 1, 1, false, false, false)
+ +Item(3, 0, 0, 1440, 3120, false, false, false) {
+ +Item(4, 0, 0, 1440, 3120, false, false, false) {
+ +Item(5, 0, 0, 1440, 3120, false, false, false) {
+ +Item(6, 0, 0, 1440, 3120, false, false, false) {
+ +Item(7, 0, 0, 1440, 3120, false, false, false) {
+ +Item(8, 0, 0, 1440, 3120, false, false, false) {
+ +Item(9, 0, 0, 1440, 3120, false, false, false) {
+ +Item(10, 0, 0, 1440, 3036, false, false, false) {
+ +Item(11, 0, 0, 1440, 2812, false, false, false) {
+ +Item(12, 0, 0, 1440, 2812, false, false, false) {
+ +Item(
+ 13,
+ 0,
+ 0,
+ 1440,
+ 2812,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 14,
+ 0,
+ 145,
+ 1440,
+ 2812,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 15,
+ 0,
+ 145,
+ 1440,
+ 2812,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 16,
+ 0,
+ 145,
+ 1440,
+ 373,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 17,
+ 0,
+ 145,
+ 1440,
+ 369,
+ false,
+ false,
+ true
+ ) {
+ +Item(
+ 18,
+ 0,
+ 145,
+ 1440,
+ 369,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 19,
+ 14,
+ 173,
+ 182,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 20,
+ 14,
+ 173,
+ 182,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 21,
+ 14,
+ 173,
+ 182,
+ 341,
+ false,
+ false,
+ true
+ ) {
+ +Item(
+ 22,
+ 14,
+ 173,
+ 182,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 23,
+ 28,
+ 187,
+ 168,
+ 327,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 24,
+ 56,
+ 215,
+ 140,
+ 299,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ }
+ +Item(
+ 25,
+ 196,
+ -1076,
+ 1076,
+ 1591,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 26,
+ 196,
+ -1076,
+ 1076,
+ 1591,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 27,
+ 196,
+ 216,
+ 337,
+ 300,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 28,
+ 1090,
+ 173,
+ 1426,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 29,
+ 1090,
+ 173,
+ 1426,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 30,
+ 1090,
+ 173,
+ 1426,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 31,
+ 1090,
+ 173,
+ 1426,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 32,
+ 1090,
+ 173,
+ 1258,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 33,
+ 1090,
+ 173,
+ 1258,
+ 341,
+ false,
+ false,
+ true
+ ) {
+ +Item(
+ 34,
+ 1090,
+ 173,
+ 1258,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 35,
+ 1104,
+ 187,
+ 1244,
+ 327,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 36,
+ 1132,
+ 215,
+ 1216,
+ 299,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ +Item(
+ 37,
+ 1258,
+ 173,
+ 1426,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 38,
+ 1258,
+ 173,
+ 1426,
+ 341,
+ false,
+ false,
+ true
+ ) {
+ +Item(
+ 39,
+ 1258,
+ 173,
+ 1426,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 40,
+ 1272,
+ 187,
+ 1412,
+ 327,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 41,
+ 1300,
+ 215,
+ 1384,
+ 299,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ +Item(
+ 42,
+ 0,
+ 369,
+ 1440,
+ 373,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 43,
+ 0,
+ 373,
+ 1440,
+ 2812,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 44,
+ 0,
+ 373,
+ 1440,
+ 2812,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 45,
+ 0,
+ 373,
+ 1440,
+ 2812,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 46,
+ 0,
+ 373,
+ 1440,
+ 2812,
+ true,
+ false,
+ true
+ ) {
+ +Item(
+ 47,
+ 0,
+ 1877,
+ 1440,
+ 2735,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 48,
+ 0,
+ 1877,
+ 1440,
+ 2693,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 49,
+ 84,
+ 1877,
+ 1356,
+ 2073,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 50,
+ 84,
+ 1947,
+ 354,
+ 2003,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 51,
+ 84,
+ 1947,
+ 302,
+ 2003,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 52,
+ 354,
+ 1933,
+ 1188,
+ 2017,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 53,
+ 354,
+ 1933,
+ 1012,
+ 2017,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 54,
+ 354,
+ 1933,
+ 1012,
+ 2017,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 55,
+ 1258,
+ 1933,
+ 1342,
+ 2017,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 56,
+ 1258,
+ 1933,
+ 1342,
+ 2017,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 57,
+ 0,
+ 2073,
+ 1440,
+ 2693,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 58,
+ 0,
+ 2073,
+ 1440,
+ 2693,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 59,
+ 0,
+ 2073,
+ 1440,
+ 2693,
+ true,
+ false,
+ true
+ ) {
+ +Item(
+ 60,
+ 945,
+ 2073,
+ 1400,
+ 2693,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 61,
+ 966,
+ 2074,
+ 1379,
+ 2703,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 62,
+ 966,
+ 2074,
+ 1379,
+ 2487,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 63,
+ 966,
+ 2074,
+ 1379,
+ 2487,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 64,
+ 966,
+ 2515,
+ 1379,
+ 2703,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 65,
+ 490,
+ 2073,
+ 945,
+ 2693,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 66,
+ 511,
+ 2074,
+ 924,
+ 2703,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 67,
+ 511,
+ 2074,
+ 924,
+ 2487,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 68,
+ 511,
+ 2074,
+ 924,
+ 2487,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 69,
+ 511,
+ 2515,
+ 924,
+ 2703,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 70,
+ 1400,
+ 2073,
+ 1855,
+ 2693,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 71,
+ 1421,
+ 2074,
+ 1834,
+ 2703,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 72,
+ 1421,
+ 2074,
+ 1834,
+ 2487,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 73,
+ 1421,
+ 2074,
+ 1834,
+ 2487,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 74,
+ 1421,
+ 2515,
+ 1834,
+ 2703,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 75,
+ 35,
+ 2073,
+ 490,
+ 2693,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 76,
+ 56,
+ 2074,
+ 469,
+ 2703,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 77,
+ 56,
+ 2074,
+ 469,
+ 2487,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 78,
+ 56,
+ 2074,
+ 469,
+ 2487,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 79,
+ 56,
+ 2515,
+ 469,
+ 2703,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ +Item(
+ 80,
+ 0,
+ 2777,
+ 1440,
+ 2993,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 81,
+ 84,
+ 2777,
+ 1356,
+ 2993,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 82,
+ 84,
+ 2777,
+ 1188,
+ 2993,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 83,
+ 84,
+ 2777,
+ 280,
+ 2973,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 84,
+ 84,
+ 2777,
+ 280,
+ 2973,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 85,
+ 336,
+ 2777,
+ 1188,
+ 2993,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 86,
+ 1188,
+ 2777,
+ 1356,
+ 2945,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 87,
+ 1272,
+ 2826,
+ 1356,
+ 2910,
+ false,
+ true,
+ false
+ )
+ }
+ }
+ }
+ +Item(
+ 88,
+ 0,
+ 373,
+ 1440,
+ 1849,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 89,
+ 84,
+ 429,
+ 1356,
+ 645,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 90,
+ 84,
+ 436,
+ 280,
+ 632,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 91,
+ 84,
+ 436,
+ 280,
+ 632,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 92,
+ 336,
+ 436,
+ 950,
+ 652,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 93,
+ 992,
+ 429,
+ 1356,
+ 639,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 94,
+ 992,
+ 429,
+ 1356,
+ 597,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 95,
+ 992,
+ 429,
+ 1356,
+ 597,
+ false,
+ false,
+ true
+ ) {
+ +Item(
+ 96,
+ 992,
+ 443,
+ 1356,
+ 583,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 97,
+ 1048,
+ 443,
+ 1356,
+ 583,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 98,
+ 1048,
+ 478,
+ 1187,
+ 548,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 99,
+ 1048,
+ 478,
+ 1187,
+ 548,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 100,
+ 1229,
+ 443,
+ 1356,
+ 583,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 101,
+ 1229,
+ 443,
+ 1233,
+ 583,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 102,
+ 1233,
+ 443,
+ 1356,
+ 583,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 103,
+ 1256,
+ 482,
+ 1319,
+ 545,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ +Item(
+ 104,
+ 1056,
+ 429,
+ 1293,
+ 639,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 105,
+ 1056,
+ 583,
+ 1293,
+ 639,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ +Item(
+ 106,
+ 0,
+ 687,
+ 1440,
+ 1849,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 107,
+ 0,
+ 687,
+ 1440,
+ 1849,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 108,
+ 0,
+ 687,
+ 1440,
+ 911,
+ true,
+ false,
+ false
+ ) {
+ +Item(
+ 109,
+ 0,
+ 799,
+ 84,
+ 799,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 110,
+ 84,
+ 729,
+ 377,
+ 869,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 111,
+ 171,
+ 733,
+ 291,
+ 803,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 112,
+ 171,
+ 733,
+ 242,
+ 803,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 113,
+ 249,
+ 747,
+ 291,
+ 789,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 114,
+ 84,
+ 810,
+ 377,
+ 866,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 115,
+ 338,
+ 818,
+ 377,
+ 857,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 116,
+ 377,
+ 799,
+ 433,
+ 799,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 117,
+ 433,
+ 762,
+ 437,
+ 837,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 118,
+ 437,
+ 799,
+ 493,
+ 799,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 119,
+ 493,
+ 729,
+ 839,
+ 869,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 120,
+ 631,
+ 733,
+ 701,
+ 803,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 121,
+ 631,
+ 733,
+ 701,
+ 803,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 122,
+ 493,
+ 810,
+ 839,
+ 866,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 123,
+ 799,
+ 818,
+ 838,
+ 857,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 124,
+ 839,
+ 799,
+ 895,
+ 799,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 125,
+ 895,
+ 762,
+ 899,
+ 837,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 126,
+ 899,
+ 799,
+ 955,
+ 799,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 127,
+ 955,
+ 729,
+ 1235,
+ 869,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 128,
+ 1060,
+ 733,
+ 1130,
+ 803,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 129,
+ 1060,
+ 733,
+ 1130,
+ 803,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 130,
+ 1022,
+ 810,
+ 1168,
+ 866,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 131,
+ 1129,
+ 818,
+ 1168,
+ 857,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 132,
+ 1235,
+ 799,
+ 1291,
+ 799,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 133,
+ 1291,
+ 762,
+ 1295,
+ 837,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 134,
+ 1295,
+ 799,
+ 1351,
+ 799,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 135,
+ 1351,
+ 729,
+ 1631,
+ 869,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 136,
+ 1451,
+ 733,
+ 1532,
+ 803,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 137,
+ 1451,
+ 733,
+ 1532,
+ 803,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 138,
+ 1383,
+ 810,
+ 1599,
+ 866,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 139,
+ 1631,
+ 799,
+ 1687,
+ 799,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 140,
+ 0,
+ 911,
+ 1440,
+ 1506,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 141,
+ 0,
+ 911,
+ 1440,
+ 1506,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 142,
+ 0,
+ 911,
+ 1440,
+ 1506,
+ true,
+ false,
+ true
+ ) {
+ +Item(
+ 143,
+ 1348,
+ 911,
+ 2644,
+ 1506,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 144,
+ 1380,
+ 911,
+ 2644,
+ 1506,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 145,
+ 1380,
+ 911,
+ 2644,
+ 1506,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 146,
+ 1380,
+ 911,
+ 1975,
+ 1506,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 147,
+ 2031,
+ 911,
+ 2588,
+ 1506,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 148,
+ 2031,
+ 967,
+ 2588,
+ 1023,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 149,
+ 2031,
+ 1051,
+ 2588,
+ 1121,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 150,
+ 2031,
+ 1149,
+ 2588,
+ 1289,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 151,
+ 2031,
+ 1345,
+ 2031,
+ 1506,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ +Item(
+ 152,
+ 84,
+ 911,
+ 1348,
+ 1506,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 153,
+ 84,
+ 911,
+ 1348,
+ 1506,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 154,
+ 84,
+ 911,
+ 1348,
+ 1506,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 155,
+ 84,
+ 911,
+ 679,
+ 1506,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 156,
+ 735,
+ 911,
+ 1292,
+ 1506,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 157,
+ 735,
+ 967,
+ 1292,
+ 1023,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 158,
+ 735,
+ 1051,
+ 1292,
+ 1191,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 159,
+ 735,
+ 1219,
+ 1292,
+ 1359,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 160,
+ 735,
+ 1415,
+ 735,
+ 1506,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ +Item(
+ 161,
+ 0,
+ 1506,
+ 1440,
+ 1849,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 162,
+ 84,
+ 1548,
+ 1356,
+ 1821,
+ false,
+ false,
+ true
+ ) {
+ +Item(
+ 163,
+ 84,
+ 1548,
+ 1356,
+ 1821,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 164,
+ 140,
+ 1548,
+ 1300,
+ 1821,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 165,
+ 140,
+ 1604,
+ 927,
+ 1765,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 166,
+ 140,
+ 1604,
+ 865,
+ 1688,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 167,
+ 140,
+ 1702,
+ 469,
+ 1765,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 168,
+ 140,
+ 1702,
+ 203,
+ 1765,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 169,
+ 217,
+ 1706,
+ 469,
+ 1762,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 170,
+ 983,
+ 1580,
+ 1300,
+ 1790,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 171,
+ 1040,
+ 1615,
+ 1243,
+ 1755,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 172,
+ 1040,
+ 1654,
+ 1103,
+ 1717,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 173,
+ 1103,
+ 1650,
+ 1243,
+ 1720,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 174,
+ 1117,
+ 1650,
+ 1243,
+ 1720,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ +Item(
+ 175,
+ 42,
+ 2770,
+ 1398,
+ 2770,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(176, 0, 2812, 1440, 3036, false, false, false) {
+ +Item(
+ 177,
+ 0,
+ 2812,
+ 1440,
+ 3036,
+ false,
+ false,
+ true
+ ) {
+ +Item(
+ 178,
+ 0,
+ 2812,
+ 1440,
+ 3036,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 179,
+ 0,
+ 2812,
+ 1440,
+ 3036,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 180,
+ 0,
+ 2812,
+ 288,
+ 3036,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 181,
+ 0,
+ 2812,
+ 288,
+ 3036,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 182,
+ 46,
+ 2847,
+ 242,
+ 2931,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 183,
+ 102,
+ 2847,
+ 186,
+ 2931,
+ false,
+ true,
+ false
+ )
+ }
+ +Item(
+ 184,
+ 14,
+ 2931,
+ 274,
+ 3001,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 185,
+ 74,
+ 2945,
+ 214,
+ 3001,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ +Item(
+ 186,
+ 288,
+ 2812,
+ 576,
+ 3036,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 187,
+ 288,
+ 2812,
+ 576,
+ 3036,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 188,
+ 334,
+ 2847,
+ 530,
+ 2931,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 189,
+ 390,
+ 2847,
+ 474,
+ 2931,
+ false,
+ true,
+ false
+ )
+ }
+ +Item(
+ 190,
+ 302,
+ 2931,
+ 562,
+ 3001,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 191,
+ 381,
+ 2945,
+ 484,
+ 3001,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ +Item(
+ 192,
+ 576,
+ 2812,
+ 864,
+ 3036,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 193,
+ 576,
+ 2812,
+ 864,
+ 3036,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 194,
+ 622,
+ 2847,
+ 818,
+ 2931,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 195,
+ 678,
+ 2847,
+ 762,
+ 2931,
+ false,
+ true,
+ false
+ )
+ }
+ +Item(
+ 196,
+ 590,
+ 2931,
+ 850,
+ 3001,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 197,
+ 649,
+ 2945,
+ 792,
+ 3001,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ +Item(
+ 198,
+ 864,
+ 2812,
+ 1152,
+ 3036,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 199,
+ 864,
+ 2812,
+ 1152,
+ 3036,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 200,
+ 910,
+ 2847,
+ 1106,
+ 2931,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 201,
+ 966,
+ 2847,
+ 1050,
+ 2931,
+ false,
+ true,
+ false
+ )
+ }
+ +Item(
+ 202,
+ 878,
+ 2931,
+ 1138,
+ 3001,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 203,
+ 964,
+ 2945,
+ 1052,
+ 3001,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ +Item(
+ 204,
+ 1152,
+ 2812,
+ 1440,
+ 3036,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 205,
+ 1152,
+ 2812,
+ 1440,
+ 3036,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 206,
+ 1198,
+ 2847,
+ 1394,
+ 2931,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 207,
+ 1254,
+ 2847,
+ 1338,
+ 2931,
+ false,
+ true,
+ false
+ )
+ }
+ +Item(
+ 208,
+ 1166,
+ 2931,
+ 1426,
+ 3001,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 209,
+ 1235,
+ 2945,
+ 1358,
+ 3001,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ +Item(210, 0, 0, 1440, 3120, false, false, false) {
+ +Item(211, 102, 2847, 186, 2931, false, false, false) {
+ +Item(
+ 212,
+ 102,
+ 2847,
+ 186,
+ 2931,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(213, 390, 2847, 474, 2931, false, false, false) {
+ +Item(
+ 214,
+ 390,
+ 2847,
+ 474,
+ 2931,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(215, 678, 2847, 762, 2931, false, false, false) {
+ +Item(
+ 216,
+ 678,
+ 2847,
+ 762,
+ 2931,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(217, 966, 2847, 1050, 2931, false, false, false) {
+ +Item(
+ 218,
+ 966,
+ 2847,
+ 1050,
+ 2931,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 219,
+ 1254,
+ 2847,
+ 1338,
+ 2931,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 220,
+ 1254,
+ 2847,
+ 1338,
+ 2931,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(221, 0, 0, 1, 1, false, false, false) {
+ +Item(222, 0, 0, 1440, 196, false, false, false)
+ }
+ }
+ }
+ +Item(223, 0, 0, 1440, 3120, false, false, false) {
+ +Item(224, 636, 1476, 804, 1644, false, false, false)
+ }
+ +Item(225, 0, 0, 1, 1, false, false, false)
+ }
+ +Item(226, 0, 0, 1440, 145, false, false, false) {
+ +Item(227, 0, 145, 1440, 145, false, false, false)
+ }
+ }
+ }
+ +Item(228, 0, 0, 1, 1, false, false, false)
+ }
+ }
+ }
+ +Item(229, 0, 3036, 1440, 3120, false, false, false)
+ +Item(230, 0, 0, 1440, 145, false, false, false)
+ }
+
+val exampleLayoutRects: Array<IntArray> = run {
+ val emptyIntArray = IntArray(0)
+ val results = Array(231) { emptyIntArray }
+
+ fun push(item: Item) {
+ results[item.id] = item.bounds
+ item.children.forEach { child -> push(child) }
+ }
+ push(rootItem)
+ for (bounds in results) {
+ assert(bounds !== emptyIntArray)
+ }
+ results
+}
+
+val scrollableItems: List<Item> = run {
+ val results = mutableListOf<Item>()
+
+ fun traverse(item: Item) {
+ if (item.scrollable) {
+ results.add(item)
+ }
+ item.children.forEach { child -> traverse(child) }
+ }
+ traverse(rootItem)
+ results
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposeUiFlags.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposeUiFlags.kt
new file mode 100644
index 0000000..536ef76
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposeUiFlags.kt
@@ -0,0 +1,66 @@
+/*
+ * 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:JvmName("ComposeRuntimeFlags")
+
+package androidx.compose.ui
+
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmName
+
+/**
+ * This is a collection of flags which are used to guard against regressions in some of the
+ * "riskier" refactors or new feature support that is added to this module. These flags are always
+ * "on" in the published artifact of this module, however these flags allow end consumers of this
+ * module to toggle them "off" in case this new path is causing a regression.
+ *
+ * These flags are considered temporary, and there should be no expectation for these flags be
+ * around for an extended period of time. If you have a regression that one of these flags fixes, it
+ * is strongly encouraged for you to file a bug ASAP.
+ *
+ * **Usage:**
+ *
+ * In order to turn a feature off in a debug environment, it is recommended to set this to false in
+ * as close to the initial loading of the application as possible. Changing this value after compose
+ * library code has already been loaded can result in undefined behavior.
+ *
+ * class MyApplication : Application() {
+ * override fun onCreate() {
+ * ComposeUiFlags.SomeFeatureEnabled = false
+ * super.onCreate()
+ * }
+ * }
+ *
+ * In order to turn this off in a release environment, it is recommended to additionally utilize R8
+ * rules which force a single value for the entire build artifact. This can result in the new code
+ * paths being completely removed from the artifact, which can often have nontrivial positive
+ * performance impact.
+ *
+ * -assumevalues class androidx.compose.runtime.ComposeUiFlags {
+ * public static int isRectTrackingEnabled return false
+ * }
+ */
+@ExperimentalComposeUiApi
+object ComposeUiFlags {
+ /**
+ * With this flag on, during layout we will do some additional work to store the minimum
+ * bounding rectangles for all Layout Nodes. This introduces some additional maintenance burden,
+ * but will be used in the future to enable certain features that are not possible to do
+ * efficiently at this point, as well as speed up some other areas of the system such as
+ * semantics, focus, pointer input, etc. If significant performance overhead is noticed during
+ * layout phases, it is possible that the addition of this tracking is the culprit.
+ */
+ @Suppress("MutableBareField") @JvmField var isRectTrackingEnabled: Boolean = true
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachMeasureScope.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachMeasureScope.kt
index 8010fb9..d64d81b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachMeasureScope.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachMeasureScope.kt
@@ -90,7 +90,7 @@
override fun layout(
width: Int,
height: Int,
- alignmentLines: Map<AlignmentLine, Int>,
+ alignmentLines: Map<out AlignmentLine, Int>,
rulers: (RulerScope.() -> Unit)?,
placementBlock: Placeable.PlacementScope.() -> Unit
): MeasureResult {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
index dc853ae..1bdba48 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
@@ -351,7 +351,7 @@
override fun layout(
width: Int,
height: Int,
- alignmentLines: Map<AlignmentLine, Int>,
+ alignmentLines: Map<out AlignmentLine, Int>,
rulers: (RulerScope.() -> Unit)?,
placementBlock: Placeable.PlacementScope.() -> Unit
): MeasureResult {
@@ -365,7 +365,7 @@
override val height: Int
get() = h
- override val alignmentLines: Map<AlignmentLine, Int>
+ override val alignmentLines: Map<out AlignmentLine, Int>
get() = alignmentLines
override val rulers: (RulerScope.() -> Unit)?
@@ -385,7 +385,7 @@
override fun layout(
width: Int,
height: Int,
- alignmentLines: Map<AlignmentLine, Int>,
+ alignmentLines: Map<out AlignmentLine, Int>,
rulers: (RulerScope.() -> Unit)?,
placementBlock: Placeable.PlacementScope.() -> Unit
): MeasureResult {
@@ -399,7 +399,7 @@
override val height: Int
get() = h
- override val alignmentLines: Map<AlignmentLine, Int>
+ override val alignmentLines: Map<out AlignmentLine, Int>
get() = alignmentLines
override val rulers: (RulerScope.() -> Unit)?
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureResult.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureResult.kt
index a2b16cc..e7a3137 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureResult.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureResult.kt
@@ -19,7 +19,7 @@
* Alignment lines that can be used by parents to align this layout. This only includes the
* alignment lines of this layout and not children.
*/
- val alignmentLines: Map<AlignmentLine, Int>
+ val alignmentLines: Map<out AlignmentLine, Int>
/**
* An optional lambda function used to create [Ruler]s for child layout. This may be
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureScope.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureScope.kt
index 36e7ea8..978165e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureScope.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureScope.kt
@@ -47,7 +47,7 @@
fun layout(
width: Int,
height: Int,
- alignmentLines: Map<AlignmentLine, Int> = emptyMap(),
+ alignmentLines: Map<out AlignmentLine, Int> = emptyMap(),
placementBlock: Placeable.PlacementScope.() -> Unit
) = layout(width, height, alignmentLines, null, placementBlock)
@@ -69,7 +69,7 @@
fun layout(
width: Int,
height: Int,
- alignmentLines: Map<AlignmentLine, Int> = emptyMap(),
+ alignmentLines: Map<out AlignmentLine, Int> = emptyMap(),
rulers: (RulerScope.() -> Unit)? = null,
placementBlock: Placeable.PlacementScope.() -> Unit
): MeasureResult {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
index 442dada..08a5f0c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
@@ -911,7 +911,7 @@
override fun layout(
width: Int,
height: Int,
- alignmentLines: Map<AlignmentLine, Int>,
+ alignmentLines: Map<out AlignmentLine, Int>,
rulers: (RulerScope.() -> Unit)?,
placementBlock: Placeable.PlacementScope.() -> Unit
): MeasureResult {
@@ -923,7 +923,7 @@
override val height: Int
get() = height
- override val alignmentLines: Map<AlignmentLine, Int>
+ override val alignmentLines: Map<out AlignmentLine, Int>
get() = alignmentLines
override val rulers: (RulerScope.() -> Unit)?
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 ffc88f5..f967cd8 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
@@ -99,48 +99,33 @@
return null
}
-internal inline fun DelegatableNode.visitSubtree(mask: Int, block: (Modifier.Node) -> Unit) {
- // TODO(lmr): we might want to add some safety wheels to prevent this from being called
- // while one of the chains is being diffed / updated.
- checkPrecondition(node.isAttached) { "visitSubtree called on an unattached node" }
- var node: Modifier.Node? = node.child
- var layout: LayoutNode? = requireLayoutNode()
- // we use this bespoke data structure here specifically for traversing children. In the
- // depth first traversal you would typically do a `stack.addAll(node.children)` type
- // call, but to avoid enumerating the vector and moving into our stack, we simply keep
- // a stack of vectors and keep track of where we are in each
- val nodes = NestedVectorStack<LayoutNode>()
- while (layout != null) {
- // NOTE: the ?: is important here for the starting condition, since we are starting
- // at THIS node, and not the head of this node chain.
- node = node ?: layout.nodes.head
- if (node.aggregateChildKindSet and mask != 0) {
- while (node != null) {
- if (node.kindSet and mask != 0) {
- block(node)
- }
- node = node.child
- }
- }
- node = null
- nodes.push(layout._children)
- layout = if (nodes.isNotEmpty()) nodes.pop() else null
+private fun LayoutNode.getChildren(zOrder: Boolean) =
+ if (zOrder) {
+ zSortedChildren
+ } else {
+ _children
}
+
+private fun MutableVector<Modifier.Node>.addLayoutNodeChildren(
+ node: Modifier.Node,
+ zOrder: Boolean,
+) {
+ node.requireLayoutNode().getChildren(zOrder).forEachReversed { add(it.nodes.head) }
}
-private fun MutableVector<Modifier.Node>.addLayoutNodeChildren(node: Modifier.Node) {
- node.requireLayoutNode()._children.forEachReversed { add(it.nodes.head) }
-}
-
-internal inline fun DelegatableNode.visitChildren(mask: Int, block: (Modifier.Node) -> Unit) {
+internal inline fun DelegatableNode.visitChildren(
+ mask: Int,
+ zOrder: Boolean,
+ block: (Modifier.Node) -> Unit
+) {
check(node.isAttached) { "visitChildren called on an unattached node" }
val branches = mutableVectorOf<Modifier.Node>()
val child = node.child
- if (child == null) branches.addLayoutNodeChildren(node) else branches.add(child)
+ if (child == null) branches.addLayoutNodeChildren(node, zOrder) else branches.add(child)
while (branches.isNotEmpty()) {
val branch = branches.removeAt(branches.lastIndex)
if (branch.aggregateChildKindSet and mask == 0) {
- branches.addLayoutNodeChildren(branch)
+ branches.addLayoutNodeChildren(branch, zOrder)
// none of these nodes match the mask, so don't bother traversing them
continue
}
@@ -159,11 +144,15 @@
* visit the shallow tree of children of a given mask, but if block returns true, we will continue
* traversing below it
*/
-internal inline fun DelegatableNode.visitSubtreeIf(mask: Int, block: (Modifier.Node) -> Boolean) {
+internal inline fun DelegatableNode.visitSubtreeIf(
+ mask: Int,
+ zOrder: Boolean,
+ block: (Modifier.Node) -> Boolean
+) {
checkPrecondition(node.isAttached) { "visitSubtreeIf called on an unattached node" }
val branches = mutableVectorOf<Modifier.Node>()
val child = node.child
- if (child == null) branches.addLayoutNodeChildren(node) else branches.add(child)
+ if (child == null) branches.addLayoutNodeChildren(node, zOrder) else branches.add(child)
outer@ while (branches.isNotEmpty()) {
val branch = branches.removeAt(branches.size - 1)
if (branch.aggregateChildKindSet and mask != 0) {
@@ -176,7 +165,7 @@
node = node.child
}
}
- branches.addLayoutNodeChildren(branch)
+ branches.addLayoutNodeChildren(branch, zOrder)
}
}
@@ -264,33 +253,41 @@
return null
}
-internal inline fun <reified T> DelegatableNode.visitSubtree(
- type: NodeKind<T>,
- block: (T) -> Unit
-) = visitSubtree(type.mask) { it.dispatchForKind(type, block) }
-
internal inline fun <reified T> DelegatableNode.visitChildren(
type: NodeKind<T>,
+ zOrder: Boolean = false,
block: (T) -> Unit
-) = visitChildren(type.mask) { it.dispatchForKind(type, block) }
+) = visitChildren(type.mask, zOrder) { it.dispatchForKind(type, block) }
internal inline fun <reified T> DelegatableNode.visitSelfAndChildren(
type: NodeKind<T>,
+ zOrder: Boolean = false,
block: (T) -> Unit
) {
node.dispatchForKind(type, block)
- visitChildren(type.mask) { it.dispatchForKind(type, block) }
+ visitChildren(type.mask, zOrder) { it.dispatchForKind(type, block) }
}
internal inline fun <reified T> DelegatableNode.visitSubtreeIf(
type: NodeKind<T>,
+ zOrder: Boolean = false,
block: (T) -> Boolean
) =
- visitSubtreeIf(type.mask) foo@{ node ->
+ visitSubtreeIf(type.mask, zOrder) foo@{ node ->
node.dispatchForKind(type) { if (!block(it)) return@foo false }
true
}
+internal inline fun <reified T> DelegatableNode.visitSubtree(
+ type: NodeKind<T>,
+ zOrder: Boolean = false,
+ block: (T) -> Unit
+) =
+ visitSubtreeIf(type.mask, zOrder) {
+ it.dispatchForKind(type, block)
+ true
+ }
+
internal fun DelegatableNode.has(type: NodeKind<*>): Boolean =
node.aggregateChildKindSet and type.mask != 0
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index 92695e5..4229de0 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -59,6 +59,8 @@
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.viewinterop.InteropView
import androidx.compose.ui.viewinterop.InteropViewFactoryHolder
@@ -91,6 +93,11 @@
InteroperableComposeUiNode,
Owner.OnLayoutCompletedListener {
+ internal var offsetFromRoot: IntOffset = IntOffset.Max
+ internal var lastSize: IntSize = IntSize.Zero
+ internal var outerToInnerOffset: IntOffset = IntOffset.Max
+ internal var outerToInnerOffsetDirty: Boolean = true
+
var forceUseOldLayers: Boolean = false
override var compositeKeyHash: Int = 0
@@ -814,7 +821,7 @@
/** The inner-most layer coordinator. Used for performance for NodeCoordinator.findLayer(). */
private var _innerLayerCoordinator: NodeCoordinator? = null
internal var innerLayerCoordinatorIsDirty = true
- private val innerLayerCoordinator: NodeCoordinator?
+ internal val innerLayerCoordinator: NodeCoordinator?
get() {
if (innerLayerCoordinatorIsDirty) {
var coordinator: NodeCoordinator? = innerCoordinator
@@ -1056,6 +1063,7 @@
* measurement need to be re-done. Such events include modifier change, attach/detach, etc.
*/
internal fun invalidateMeasurements() {
+ outerToInnerOffsetDirty = true
if (lookaheadRoot != null) {
requestLookaheadRemeasure()
} else {
@@ -1077,6 +1085,7 @@
/** Used to request a new layout pass from the owner. */
internal fun requestRelayout(forceRequest: Boolean = false) {
+ outerToInnerOffsetDirty = true
if (!isVirtual) {
owner?.onRequestRelayout(this, forceRequest = forceRequest)
}
@@ -1325,6 +1334,7 @@
if (isAttached) {
invalidateSemantics()
}
+ owner?.onLayoutNodeDeactivated(this)
}
override fun onRelease() {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt
index a4d667fb..fe22dff 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt
@@ -94,9 +94,9 @@
/** The alignment lines of this layout, inherited + intrinsic */
private val alignmentLineMap: MutableMap<AlignmentLine, Int> = hashMapOf()
- fun getLastCalculation(): Map<AlignmentLine, Int> = alignmentLineMap
+ fun getLastCalculation(): Map<out AlignmentLine, Int> = alignmentLineMap
- protected abstract val NodeCoordinator.alignmentLinesMap: Map<AlignmentLine, Int>
+ protected abstract val NodeCoordinator.alignmentLinesMap: Map<out AlignmentLine, Int>
protected abstract fun NodeCoordinator.getPositionFor(alignmentLine: AlignmentLine): Int
@@ -201,7 +201,7 @@
internal class LayoutNodeAlignmentLines(alignmentLinesOwner: AlignmentLinesOwner) :
AlignmentLines(alignmentLinesOwner) {
- override val NodeCoordinator.alignmentLinesMap: Map<AlignmentLine, Int>
+ override val NodeCoordinator.alignmentLinesMap: Map<out AlignmentLine, Int>
get() = measureResult.alignmentLines
override fun NodeCoordinator.getPositionFor(alignmentLine: AlignmentLine): Int =
@@ -215,7 +215,7 @@
internal class LookaheadAlignmentLines(alignmentLinesOwner: AlignmentLinesOwner) :
AlignmentLines(alignmentLinesOwner) {
- override val NodeCoordinator.alignmentLinesMap: Map<AlignmentLine, Int>
+ override val NodeCoordinator.alignmentLinesMap: Map<out AlignmentLine, Int>
get() = lookaheadDelegate!!.measureResult.alignmentLines
override fun NodeCoordinator.getPositionFor(alignmentLine: AlignmentLine): Int =
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
index 3df4e68..349e94f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
@@ -841,6 +841,7 @@
}
layoutState = LayoutState.LayingOut
+ val firstPlacement = !placedOnce
lastPosition = position
lastZIndex = zIndex
lastLayerBlock = layerBlock
@@ -849,6 +850,7 @@
onNodePlacedCalled = false
val owner = layoutNode.requireOwner()
+ owner.rectManager.onLayoutPositionChanged(layoutNode, position, firstPlacement)
if (!layoutPending && isPlaced) {
outerCoordinator.placeSelfApparentToRealOffset(position, zIndex, layerBlock, layer)
onNodePlaced()
@@ -950,7 +952,7 @@
return true
}
- override fun calculateAlignmentLines(): Map<AlignmentLine, Int> {
+ override fun calculateAlignmentLines(): Map<out AlignmentLine, Int> {
if (!duringAlignmentLinesQuery) {
// Mark alignments used by modifier
if (layoutState == LayoutState.Measuring) {
@@ -1278,7 +1280,7 @@
}
}
- override fun calculateAlignmentLines(): Map<AlignmentLine, Int> {
+ override fun calculateAlignmentLines(): Map<out AlignmentLine, Int> {
if (!duringAlignmentLinesQuery) {
if (layoutState == LayoutState.LookaheadMeasuring) {
// Mark alignments used by modifier
@@ -1894,7 +1896,7 @@
fun layoutChildren()
/** Recalculate the alignment lines if dirty, and layout children as needed. */
- fun calculateAlignmentLines(): Map<AlignmentLine, Int>
+ fun calculateAlignmentLines(): Map<out AlignmentLine, Int>
/**
* Parent [AlignmentLinesOwner]. This will be the AlignmentLinesOwner for the same pass but for
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt
index f5e4439..8bf8d59 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt
@@ -203,7 +203,7 @@
override fun layout(
width: Int,
height: Int,
- alignmentLines: Map<AlignmentLine, Int>,
+ alignmentLines: Map<out AlignmentLine, Int>,
rulers: (RulerScope.() -> Unit)?,
placementBlock: PlacementScope.() -> Unit
): MeasureResult {
@@ -215,7 +215,7 @@
override val height: Int
get() = height
- override val alignmentLines: Map<AlignmentLine, Int>
+ override val alignmentLines: Map<out AlignmentLine, Int>
get() = alignmentLines
override val rulers: (RulerScope.() -> Unit)?
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NestedVectorStack.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NestedVectorStack.kt
deleted file mode 100644
index 7f93d07..0000000
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NestedVectorStack.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright 2022 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.runtime.collection.MutableVector
-
-internal class NestedVectorStack<T> {
- // number of vectors in the stack
- private var size = 0
- // holds the current "top" index for each vector
- private var currentIndexes = IntArray(16)
- private var vectors = arrayOfNulls<MutableVector<T>>(16)
-
- fun isNotEmpty(): Boolean {
- return size > 0 && currentIndexes[size - 1] >= 0
- }
-
- fun pop(): T {
- check(size > 0) { "Cannot call pop() on an empty stack. Guard with a call to isNotEmpty()" }
- val indexOfVector = size - 1
- val indexOfItem = currentIndexes[indexOfVector]
- val vector = vectors[indexOfVector]!!
- if (indexOfItem > 0) currentIndexes[indexOfVector]--
- else if (indexOfItem == 0) {
- vectors[indexOfVector] = null
- size--
- }
- return vector[indexOfItem]
- }
-
- fun push(vector: MutableVector<T>) {
- // if the vector is empty there is no reason for us to add it
- if (vector.isEmpty()) return
- val nextIndex = size
- // check to see that we have capacity to add another vector
- if (nextIndex >= currentIndexes.size) {
- currentIndexes = currentIndexes.copyOf(currentIndexes.size * 2)
- vectors = vectors.copyOf(vectors.size * 2)
- }
- currentIndexes[nextIndex] = vector.size - 1
- vectors[nextIndex] = vector
- size++
- }
-}
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 b522a01..23b9014 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
@@ -521,7 +521,13 @@
layoutNode.innerLayerCoordinatorIsDirty = true
invalidateParentLayer()
} else if (updateParameters) {
- updateLayerParameters()
+ val positionalPropertiesChanged = updateLayerParameters()
+ if (positionalPropertiesChanged) {
+ layoutNode
+ .requireOwner()
+ .rectManager
+ .onLayoutLayerPositionalPropertiesChanged(layoutNode)
+ }
}
} else {
this.layerBlock = null
@@ -1340,7 +1346,9 @@
layoutDelegate.measurePassDelegate
.notifyChildrenUsingCoordinatesWhilePlacing()
}
- layoutNode.owner?.requestOnPositionedCallback(layoutNode)
+ val owner = layoutNode.requireOwner()
+ owner.rectManager.onLayoutLayerPositionalPropertiesChanged(layoutNode)
+ owner.requestOnPositionedCallback(layoutNode)
}
}
}
@@ -1407,7 +1415,7 @@
@Suppress("PrimitiveInCollection")
private fun compareEquals(
a: MutableObjectIntMap<AlignmentLine>?,
- b: Map<AlignmentLine, Int>
+ b: Map<out AlignmentLine, Int>
): Boolean {
if (a == null) return false
if (a.size != b.size) return false
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnedLayer.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnedLayer.kt
index 6090fdb..926bc9f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnedLayer.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnedLayer.kt
@@ -91,6 +91,9 @@
*/
fun transform(matrix: Matrix)
+ /** The matrix associated with the affine transform of this layer */
+ val underlyingMatrix: Matrix
+
/**
* Calculates the transform from the layer to the parent and multiplies [matrix] by the
* transform.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
index 30d11e0..b2d7cec5 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
@@ -46,6 +46,7 @@
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.WindowInfo
+import androidx.compose.ui.spatial.RectManager
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.input.TextInputService
@@ -133,6 +134,9 @@
/** Provide information about the window that hosts this [Owner]. */
val windowInfo: WindowInfo
+ /** Provides a queryable and observable index of nodes' bounding rectangles */
+ val rectManager: RectManager
+
@Deprecated(
"fontLoader is deprecated, use fontFamilyResolver",
replaceWith = ReplaceWith("fontFamilyResolver")
@@ -258,6 +262,8 @@
/** The position and/or size of the [layoutNode] changed. */
fun onLayoutChange(layoutNode: LayoutNode)
+ fun onLayoutNodeDeactivated(layoutNode: LayoutNode)
+
/**
* The position and/or size of an interop view (typically, an android.view.View) has changed. On
* Android, this schedules view tree layout observer callback to be invoked for the underlying
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectList.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectList.kt
new file mode 100644
index 0000000..1105c4f
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectList.kt
@@ -0,0 +1,819 @@
+/*
+ * 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:Suppress("NOTHING_TO_INLINE")
+
+package androidx.compose.ui.spatial
+
+import kotlin.jvm.JvmField
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * This is a fairly straight-forward data structure. It stores an Int value and a corresponding
+ * rectangle, and allows for efficient querying based on the spatial relationships between the
+ * objects contained in it, but it does so just by storing all of the information packed into a
+ * single LongArray. Because of the simplicity and tight loops / locality of information, this ends
+ * up being faster than most other data structures in most things for the size data sets that we
+ * will be using this for. For O(10**2) items, this outperformas other data structures. Each
+ * meta/rect pair is stored contiguously as 3 Longs in an LongArray. This makes insert and update
+ * extremely cheap. Query operations require scanning the entire array, but due to cache locality
+ * and fairly efficient math, it is competitive with data structures which use mechanisms to prune
+ * the size of the data set to query less.
+ *
+ * This data structure comes with some assumptions:
+ * 1. the "identifier" values for this data structure are positive Ints. For performance reasons, we
+ * only store 26 bits of precision here, so practically speaking the item id is limited to 26
+ * bits (~67,000,000).
+ * 2. The coordinate system used for this data structure has the positive "x" axis pointing to the
+ * "right", and the positive "y" axis pointing "down". As a result, a rectangle will always have
+ * top <= bottom, and left <= right.
+ */
+@Suppress("NAME_SHADOWING")
+internal class RectList {
+ /**
+ * This is the primary data storage. We store items linearly, with each "item" taking up three
+ * longs (192 bits) of space. The partitioning generally looks like:
+ *
+ * Long 1 (64 bits): the "top left" long
+ * 32 bits: left
+ * 32 bits: top
+ * Long 2 (64 bits): the "bottom right" long
+ * 32 bits: right
+ * 32 bits: bottom
+ * Long 3 (64 bits): the "meta" long
+ * 26 bits: item id
+ * 26 bits: parent id
+ * 10 bits: last child offset
+ * 1 bits: focusable
+ * 1 bits: gesturable
+ */
+ @JvmField internal var items: LongArray = LongArray(LongsPerItem * 64) // 64 items
+
+ /**
+ * We allocate a 2nd LongArray. This is always going to be sized identical to [items], and
+ * during [defragment] we will swap the two in order to have a cheap defragment algorithm that
+ * preserves order.
+ *
+ * Additionally, this "double buffering" ends up having a side benefit where we can use this
+ * array during [updateSubhierarchy] as a local stack which will never have to grow since it
+ * cannot exceed the size of the items array itself. This allows for RectList to have as few
+ * allocations as possible, however this does double the memory footprint.
+ *
+ * @see [defragment]
+ * @see [updateSubhierarchy]
+ */
+ @JvmField internal var stack: LongArray = LongArray(LongsPerItem * 64) // 64 items
+
+ /**
+ * The size of the items array that is filled with actual data. This is different from
+ * `items.size` since the array is larger than the data it contains so that inserts can be
+ * cheap.
+ */
+ @JvmField internal var itemsSize: Int = 0
+
+ /** The number of items */
+ val size: Int
+ get() = itemsSize / LongsPerItem
+
+ /**
+ * Returns the 0th index of which 3 contiguous Longs can be stored in the items array. If space
+ * is available at the end of the array, it will use that. If not, this will grow the items
+ * array.This method will return an Int index that you can use, BUT, this method has side
+ * effects and may mutate the [items] and [itemsSize] fields on this class. It is important to
+ * keep this in mind if you call this method and have cached any of those values in a local
+ * variable, you may need to refresh them.
+ */
+ internal fun allocateItemsIndex(): Int {
+ val currentItems = items
+ val currentSize = itemsSize
+ itemsSize = currentSize + LongsPerItem
+ val actualSize = currentItems.size
+ if (actualSize <= currentSize + LongsPerItem) {
+ val newSize = max(actualSize * 2, currentSize + LongsPerItem)
+ items = currentItems.copyOf(newSize)
+ stack = stack.copyOf(newSize)
+ }
+ return currentSize
+ }
+
+ /**
+ * Insert a value and corresponding bounding rectangle into the RectList. This method does not
+ * check to see that [value] doesn't already exist somewhere in the list.
+ *
+ * NOTE: -1 is NOT a valid value for this collection since it is used as a tombstone value.
+ *
+ * @param value The value to be stored. Intended to be a layout node id. Must be a positive
+ * integer of 28 bits or less
+ * @param l the left coordinate of the rectangle
+ * @param t the top coordinate of the rectangle
+ * @param r the right coordinate of the rectangle
+ * @param b the bottom coordinate of the rectangle
+ * @param parentId If this element is inside of a "scrollable" container which we want to update
+ * with the [updateSubhierarchy] API, then this is the id of that scroll container.
+ * @param focusable true if this element is focusable. This is a flag which we can use to limit
+ * the results of certain queries for
+ * @param gesturable true if this element is a pointer input gesture detector. This is a flag
+ * which we can use to limit the results of certain queries for
+ */
+ fun insert(
+ value: Int,
+ l: Int,
+ t: Int,
+ r: Int,
+ b: Int,
+ parentId: Int = -1,
+ focusable: Boolean = false,
+ gesturable: Boolean = false,
+ ) {
+ val value = value and Lower26Bits
+ val index = allocateItemsIndex()
+ val items = items
+
+ items[index + 0] = packXY(l, t)
+ items[index + 1] = packXY(r, b)
+ items[index + 2] = packMeta(value, parentId, lastChildOffset = 0, focusable, gesturable)
+
+ if (parentId < 0) return
+ val parentId = parentId and Lower26Bits
+ // After inserting, find the item with id = parentId and update it's "last child offset".
+ var i = index - LongsPerItem
+ while (i > 0) {
+ val meta = items[i + 2]
+ if (unpackMetaValue(meta) == parentId) {
+ // TODO: right now this number will always be a multiple of 3. Since the last child
+ // offset only has 10 bits of precision, we probably want to encode this more
+ // efficiently. It doesn't have to be exact, it just can't be too small. We could
+ // obviously divide by LongsPerItem, but we may also want to do something cheaper
+ // like dividing by 2 or 4
+ val lastChildOffset = index - i
+ items[i + 2] = metaWithLastChildOffset(meta, lastChildOffset)
+ return
+ }
+ i -= LongsPerItem
+ }
+ }
+
+ /**
+ * Remove a value from this collection.
+ *
+ * @return Whether or not a value was found and removed from this list successfully.
+ * @see defragment
+ */
+ fun remove(value: Int): Boolean {
+ val value = value and Lower26Bits
+ val items = items
+ val size = itemsSize
+ var i = 0
+ while (i < items.size - 2) {
+ if (i >= size) break
+ // NOTE: We are assuming that the value can only be here once.
+ val meta = items[i + 2]
+ if (unpackMetaValue(meta) == value) {
+ // To "remove" an item, we make the rectangle [max, max, max, max] so that it won't
+ // match any queries, and we mark meta as tombStone so we can detect it later
+ // in the defragment method
+ items[i + 0] = 0xffff_ffff_ffff_ffffUL.toLong()
+ items[i + 1] = 0xffff_ffff_ffff_ffffUL.toLong()
+ items[i + 2] = TombStone
+ return true
+ }
+ i += LongsPerItem
+ }
+ return false
+ }
+
+ /**
+ * Updates the rectangle associated with this value.
+ *
+ * @return true if the value was found and updated, false if this value is not currently in the
+ * collection
+ */
+ fun update(value: Int, l: Int, t: Int, r: Int, b: Int): Boolean {
+ val value = value and Lower26Bits
+ val items = items
+ val size = itemsSize
+ var i = 0
+ while (i < items.size - 2) {
+ if (i >= size) break
+ val meta = items[i + 2]
+ // NOTE: We are assuming that the value can only be here once.
+ if (unpackMetaValue(meta) == value) {
+ items[i + 0] = packXY(l, t)
+ items[i + 1] = packXY(r, b)
+ return true
+ }
+ i += LongsPerItem
+ }
+ return false
+ }
+
+ /**
+ * Moves the rectangle associated with this value to the specified rectangle, and updates every
+ * item that is "below" the specified rectangle by the associated offset. move() is generally
+ * more efficient than calling update() for all of the rectangles included in the subhierarchy
+ * of the item.
+ */
+ fun move(value: Int, l: Int, t: Int, r: Int, b: Int): Boolean {
+ val value = value and Lower26Bits
+ val items = items
+ val size = itemsSize
+ var i = 0
+ while (i < items.size - 2) {
+ if (i >= size) break
+ val meta = items[i + 2]
+ // NOTE: We are assuming that the value can only be here once.
+ if (unpackMetaValue(meta) == value) {
+ val prevLT = items[i + 0]
+ items[i + 0] = packXY(l, t)
+ items[i + 1] = packXY(r, b)
+ val deltaX = l - unpackX(prevLT)
+ val deltaY = t - unpackY(prevLT)
+ if ((deltaX != 0) or (deltaY != 0)) {
+ updateSubhierarchy(metaWithParentId(meta, i + LongsPerItem), deltaX, deltaY)
+ }
+ return true
+ }
+ i += LongsPerItem
+ }
+ return false
+ }
+
+ fun updateSubhierarchy(id: Int, deltaX: Int, deltaY: Int) {
+ updateSubhierarchy(
+ //
+ stackMeta =
+ packMeta(
+ itemId = id,
+ parentId = 0,
+ lastChildOffset = items.size,
+ focusable = false,
+ gesturable = false,
+ ),
+ deltaX = deltaX,
+ deltaY = deltaY
+ )
+ }
+
+ /**
+ * Updates a subhierarchy of items by the specified delta. For efficiency, the [stackMeta]
+ * provided is a Long encoded with the same scheme of the "meta" long of each item, where the
+ * encoding has the following semantic specific to this method:
+ *
+ * Long (64 bits): the "stack meta" encoding
+ * 26 bits: the "parent id" that we are matching on (normally item id)
+ * 26 bits: the minimum index that a child can have (normally parent id)
+ * 10 bits: max offset from start index a child can have (normally last child offset)
+ * 1 bits: unused (normally focusable)
+ * 1 bits: unused (normally gesturable)
+ *
+ * We use this essentially as a way to encode three integers into a long, which includes all of
+ * the data needed to efficiently iterate through the below algorithm. It is effectively an id
+ * and a range. The range isn't strictly needed, but it helps turn this O(n^2) algorithm into
+ * something that is ~O(n) in the average case (still O(n^2) worst case though). By using the
+ * same encoding as "meta" longs, we only need to update the start index when we
+ */
+ private fun updateSubhierarchy(stackMeta: Long, deltaX: Int, deltaY: Int) {
+ val items = items
+ val stack = stack
+ val size = size
+ stack[0] = stackMeta
+ var stackSize = 1
+ while (stackSize > 0) {
+ val idAndStartAndOffset = stack[--stackSize]
+ val parentId = unpackMetaValue(idAndStartAndOffset) // parent id is in the id slot
+ var i = unpackMetaParentId(idAndStartAndOffset) // start index is in the parent id slot
+ val offset = unpackMetaLastChildOffset(idAndStartAndOffset)
+ val endIndex = if (offset == Lower10Bits) size else offset + i
+ if (i < 0) break
+ while (i < items.size - 2) {
+ if (i >= endIndex) break
+ val meta = items[i + 2]
+ if (unpackMetaParentId(meta) == parentId) {
+ val topLeft = items[i + 0]
+ val bottomRight = items[i + 1]
+ items[i + 0] = packXY(unpackX(topLeft) + deltaX, unpackY(topLeft) + deltaY)
+ items[i + 1] =
+ packXY(unpackX(bottomRight) + deltaX, unpackY(bottomRight) + deltaY)
+ if (unpackMetaLastChildOffset(meta) > 0) {
+ // we need to store itemId, lastChildOffset, and a "start index".
+ // For convenience, we just use `meta` which already encodes two of those
+ // values, and we add `i` into the slot for "parentId"
+ stack[stackSize++] = metaWithParentId(meta, i + LongsPerItem)
+ }
+ }
+ i += LongsPerItem
+ }
+ }
+ }
+
+ fun withRect(value: Int, block: (Int, Int, Int, Int) -> Unit): Boolean {
+ val value = value and Lower26Bits
+ val items = items
+ val size = itemsSize
+ var i = 0
+ while (i < items.size - 2) {
+ if (i >= size) break
+ val meta = items[i + 2]
+ // NOTE: We are assuming that the value can only be here once.
+ if (unpackMetaValue(meta) == value) {
+ val topLeft = items[i + 0]
+ val bottomRight = items[i + 1]
+ block(
+ unpackX(topLeft),
+ unpackY(topLeft),
+ unpackX(bottomRight),
+ unpackY(bottomRight),
+ )
+ return true
+ }
+ i += LongsPerItem
+ }
+ return false
+ }
+
+ operator fun contains(value: Int): Boolean {
+ val value = value and Lower26Bits
+ val items = items
+ val size = itemsSize
+ var i = 0
+ while (i < items.size - 2) {
+ if (i >= size) break
+ val meta = items[i + 2]
+ if (unpackMetaValue(meta) == value) {
+ return true
+ }
+ i += LongsPerItem
+ }
+ return false
+ }
+
+ fun metaFor(value: Int): Long {
+ val value = value and Lower26Bits
+ val items = items
+ val size = itemsSize
+ var i = 0
+ while (i < items.size - 2) {
+ if (i >= size) break
+ val meta = items[i + 2]
+ // NOTE: We are assuming that the value can only be here once.
+ if (unpackMetaValue(meta) == value) {
+ return meta
+ }
+ i += LongsPerItem
+ }
+ return TombStone
+ }
+
+ /**
+ * For a provided rectangle, executes [block] for each value in the collection whose associated
+ * rectangle intersects the provided one. The argument passed into [block] will be the value.
+ */
+ inline fun forEachIntersection(
+ l: Int,
+ t: Int,
+ r: Int,
+ b: Int,
+ block: (Int) -> Unit,
+ ) {
+ val destTopLeft = packXY(l, t)
+ val destTopRight = packXY(r, b)
+ val items = items
+ val size = itemsSize
+ var i = 0
+ while (i < items.size - 2) {
+ if (i >= size) break
+ val topLeft = items[i + 0]
+ val bottomRight = items[i + 1]
+ if (rectIntersectsRect(topLeft, bottomRight, destTopLeft, destTopRight)) {
+ // TODO: it might make sense to include the rectangle in the block since calling
+ // code may want to filter this list using that geometry, and it would be
+ // beneficial to not have to look up the layout node in order to do so.
+ block(unpackMetaValue(items[i + 2]))
+ }
+ i += LongsPerItem
+ }
+ }
+
+ inline fun forEachRect(
+ block: (Int, Int, Int, Int, Int) -> Unit,
+ ) {
+ val items = items
+ val size = itemsSize
+ var i = 0
+ while (i < items.size - 2) {
+ if (i >= size) break
+ val topLeft = items[i + 0]
+ val bottomRight = items[i + 1]
+ val meta = items[i + 2]
+ block(
+ unpackMetaValue(meta),
+ unpackX(topLeft),
+ unpackY(topLeft),
+ unpackX(bottomRight),
+ unpackY(bottomRight),
+ )
+ i += LongsPerItem
+ }
+ }
+
+ // TODO: add ability to filter to just gesture detectors (the main use case for this function)
+ /**
+ * For a provided point, executes [block] for each value in the collection whose associated
+ * rectangle contains the provided point. The argument passed into [block] will be the value.
+ */
+ inline fun forEachIntersection(
+ x: Int,
+ y: Int,
+ block: (Int) -> Unit,
+ ) {
+ val destXY = packXY(x, y)
+ val items = items
+ val size = itemsSize
+ var i = 0
+ while (i < items.size - 2) {
+ if (i >= size) break
+ val topLeft = items[i + 0]
+ val bottomRight = items[i + 1]
+ if (rectIntersectsRect(topLeft, bottomRight, destXY, destXY)) {
+ val meta = items[i + 2]
+ block(unpackMetaValue(meta))
+ }
+ i += LongsPerItem
+ }
+ }
+
+ internal fun neighborsScoredByDistance(
+ searchAxis: Int,
+ l: Int,
+ t: Int,
+ r: Int,
+ b: Int,
+ ): IntArray {
+ val items = items
+ val size = itemsSize / LongsPerItem
+ var i = 0
+ // build up an array of size N with each element being the score for the item at that index
+ val results = IntArray(size)
+
+ while (i < results.size) {
+ val itemsIndex = i * LongsPerItem
+ if (itemsIndex < 0 || itemsIndex >= items.size - 1) break
+ val topLeft = items[itemsIndex + 0]
+ val bottomRight = items[itemsIndex + 1]
+ val score =
+ distanceScore(
+ searchAxis,
+ l,
+ t,
+ r,
+ b,
+ unpackX(topLeft),
+ unpackY(topLeft),
+ unpackX(bottomRight),
+ unpackY(bottomRight),
+ )
+ results[i] = score
+ i++
+ }
+ return results
+ }
+
+ // TODO: add ability to filter to just focusable (the main use case for this function)
+ // TODO: add an overload which just takes in searchAxis, k, and item id
+ inline fun findKNearestNeighbors(
+ searchAxis: Int,
+ k: Int,
+ l: Int,
+ t: Int,
+ r: Int,
+ b: Int,
+ block: (score: Int, id: Int, l: Int, t: Int, r: Int, b: Int) -> Unit,
+ ) {
+ // this list is 1:1 with items and holds the score for each item
+ val list =
+ neighborsScoredByDistance(
+ searchAxis,
+ l,
+ t,
+ r,
+ b,
+ )
+ val items = items
+
+ var sent = 0
+ var min = 1
+ var nextMin = Int.MAX_VALUE
+ var loops = 0
+ var i = 0
+ while (loops <= k) {
+ while (i < list.size) {
+ val score = list[i]
+ // update nextmin if score is smaller than nextMin but larger than min
+ if (score > min) {
+ nextMin = min(nextMin, score)
+ }
+ if (score == min) {
+ val itemIndex = i * LongsPerItem
+ val topLeft = items[itemIndex + 0]
+ val bottomRight = items[itemIndex + 1]
+ val meta = items[itemIndex + 2]
+ block(
+ score,
+ unpackMetaValue(meta),
+ unpackX(topLeft),
+ unpackY(topLeft),
+ unpackX(bottomRight),
+ unpackY(bottomRight),
+ )
+ sent++
+ if (sent == k) return
+ }
+ i++
+ }
+ min = nextMin
+ nextMin = Int.MAX_VALUE
+ loops++
+ i = 0
+ }
+ }
+
+ inline fun findNearestNeighbor(searchAxis: Int, l: Int, t: Int, r: Int, b: Int): Int {
+ val items = items
+ val size = itemsSize
+ var minScore = Int.MAX_VALUE
+ var minIndex = -1
+ var i = 0
+ while (i < items.size - 2) {
+ if (i >= size) break
+ val topLeft = items[i + 0]
+ val bottomRight = items[i + 1]
+ val score =
+ distanceScore(
+ searchAxis,
+ l,
+ t,
+ r,
+ b,
+ unpackX(topLeft),
+ unpackY(topLeft),
+ unpackX(bottomRight),
+ unpackY(bottomRight),
+ )
+ val isNewMin = (score > 0) and (score < minScore)
+ minScore = if (isNewMin) score else minScore
+ minIndex = if (isNewMin) i + 1 else minIndex
+ i += LongsPerItem
+ }
+ return if (minIndex < 0 || minIndex >= items.size) {
+ -1
+ } else {
+ unpackMetaValue(items[minIndex])
+ }
+ }
+
+ /** */
+ fun defragment() {
+ val from = items
+ val size = itemsSize
+ val to = stack
+ var i = 0
+ var j = 0
+ while (i < from.size - 2) {
+ if (j >= to.size - 2) break
+ if (i >= size) break
+ if (from[i + 2] != TombStone) {
+ to[j + 0] = from[i + 0]
+ to[j + 1] = from[i + 1]
+ to[j + 2] = from[i + 2]
+ j += LongsPerItem
+ }
+ i += LongsPerItem
+ }
+ itemsSize = j
+ // NOTE: this could be a reasonable time to shrink items/stack to a smaller array if for
+ // some reason they have gotten very large. I'm choosing NOT to do this because I think
+ // if the arrays have gotten to a large size it is very likely that they will get to that
+ // size again, and avoiding the thrash here is probably desirable
+ items = to
+ stack = from
+ }
+
+ fun debugString(): String = buildString {
+ val items = items
+ val size = itemsSize
+ var i = 0
+ while (i < items.size - 2) {
+ if (i >= size) break
+ val topLeft = items[i + 0]
+ val bottomRight = items[i + 1]
+ val meta = items[i + 2]
+ val id = unpackMetaValue(meta)
+ val parentId = unpackMetaParentId(meta)
+ val l = unpackX(topLeft)
+ val t = unpackY(topLeft)
+ val r = unpackX(bottomRight)
+ val b = unpackY(bottomRight)
+ appendLine("id=$id, rect=[$l,$t,$r,$b], parent=$parentId")
+ i += LongsPerItem
+ }
+ }
+}
+
+internal const val LongsPerItem = 3
+internal const val Lower26Bits = 0b0000_0011_1111_1111_1111_1111_1111_1111
+internal const val Lower10Bits = 0b0000_0000_0000_0000_0000_0011_1111_1111
+internal const val EverythingButParentId = 0xfff0_0000_03ff_ffffUL
+internal const val EverythingButLastChildOffset = 0xc00f_ffff_ffff_ffffUL
+
+/**
+ * This is the "meta" value that we assign to every removed value.
+ *
+ * @see RectList.remove
+ * @see packMeta
+ */
+internal const val TombStone = 0x3fff_ffff_ffff_ffffL // packMeta(-1, -1, -1, false, false)
+
+internal const val AxisNorth: Int = 0
+internal const val AxisSouth: Int = 1
+internal const val AxisWest: Int = 2
+internal const val AxisEast: Int = 3
+
+internal inline fun packXY(x: Int, y: Int) = (x.toLong() shl 32) or (y.toLong() and 0xffff_ffff)
+
+internal inline fun packMeta(
+ itemId: Int,
+ parentId: Int,
+ lastChildOffset: Int,
+ focusable: Boolean,
+ gesturable: Boolean,
+): Long =
+ // 26 bits: item id
+ // 26 bits: parent id
+ // 10 bits: last child offset
+ // 1 bits: focusable
+ // 1 bits: gesturable
+ (gesturable.toLong() shl 63) or
+ (focusable.toLong() shl 62) or
+ ((lastChildOffset and Lower10Bits).toLong() shl 52) or
+ ((parentId and Lower26Bits).toLong() shl 26) or
+ ((itemId and Lower26Bits).toLong() shl 0)
+
+internal inline fun unpackMetaValue(meta: Long): Int = meta.toInt() and Lower26Bits
+
+internal inline fun unpackMetaParentId(meta: Long): Int = (meta shr 26).toInt() and Lower26Bits
+
+internal inline fun unpackMetaLastChildOffset(meta: Long): Int =
+ (meta shr 52).toInt() and Lower10Bits
+
+internal inline fun metaWithParentId(meta: Long, parentId: Int): Long =
+ (meta and EverythingButParentId.toLong()) or ((parentId and Lower26Bits).toLong() shl 26)
+
+internal inline fun metaWithLastChildOffset(meta: Long, lastChildOffset: Int): Long =
+ (meta and EverythingButLastChildOffset.toLong()) or
+ ((lastChildOffset and Lower10Bits).toLong() shl 52)
+
+internal inline fun unpackMetaFocusable(meta: Long): Int = (meta shr 62).toInt() and 0b1
+
+internal inline fun unpackMetaGesturable(meta: Long): Int = (meta shr 63).toInt() and 0b1
+
+internal inline fun unpackX(xy: Long): Int = (xy shr 32).toInt()
+
+internal inline fun unpackY(xy: Long): Int = (xy).toInt()
+
+/** */
+internal inline fun rectIntersectsRect(
+ srcLT: Long,
+ srcRB: Long,
+ destLT: Long,
+ destRB: Long
+): Boolean {
+ // destRB - srcLT = [r2 - l1, b2 - t1]
+ // srcRB - destLT = [r1 - l2, b1 - t2]
+
+ // Both of the above expressions represent two long subtractions which are effectively each two
+ // int subtractions. If any of the individual subtractions would have resulted in a negative
+ // value, then the rectangle has an intersection. If this is true, then there will be
+ // "underflow" from one 32bit component to the next, which we can detect by isolating the top
+ // bits of each component using 0x8000_0000_8000_0000UL.toLong()
+ val a = (destRB - srcLT) or (srcRB - destLT)
+ return a and 0x8000_0000_8000_0000UL.toLong() == 0L
+}
+
+/**
+ * Turns a boolean into a long of 1L/0L for true/false. It is written precisely this way as this
+ * results in a single ARM instruction where as other approaches are more expensive. For example,
+ * `if (this) 1L else 0L` is several instructions instead of just one. DO NOT change this without
+ * looking at the corresponding arm code and verifying that it is better.
+ */
+internal inline fun Boolean.toLong(): Long = (if (this) 1 else 0).toLong()
+
+/**
+ * This function will return a "score" of a rectangle relative to a query. A negative score means
+ * that the rectangle should be ignored, and a lower (but non-negative) score means that the
+ * rectangle is close and overlapping in the direction of the axis in question.
+ *
+ * @param axis the direction/axis along which we are scoring
+ * @param queryL the left of the rect we are finding the nearest neighbors of
+ * @param queryT the top of the rect we are finding the nearest neighbors of
+ * @param queryR the right of the rect we are finding the nearest neighbors of
+ * @param queryB the bottom of the rect we are finding the nearest neighbors of
+ * @param l the left of the rect which is the "neighbor" we are scoring
+ * @param t the top of the rect which is the "neighbor" we are scoring
+ * @param r the right of the rect which is the "neighbor" we are scoring
+ * @param b the bottom of the rect which is the "neighbor" we are scoring
+ * @see AxisNorth
+ * @see AxisWest
+ * @see AxisEast
+ * @see AxisSouth
+ */
+// TODO: consider just passing in TopLeft/BottomRight longs in order to reduce the number of
+// parameters here.
+internal fun distanceScore(
+ axis: Int,
+ queryL: Int,
+ queryT: Int,
+ queryR: Int,
+ queryB: Int,
+ l: Int,
+ t: Int,
+ r: Int,
+ b: Int,
+): Int {
+ return when (axis) {
+ AxisNorth ->
+ distanceScoreAlongAxis(
+ distanceMin = queryT,
+ distanceMax = b,
+ queryCrossAxisMax = queryR,
+ queryCrossAxisMin = queryL,
+ crossAxisMax = r,
+ crossAxisMin = l,
+ )
+ AxisEast ->
+ distanceScoreAlongAxis(
+ distanceMin = l,
+ distanceMax = queryR,
+ queryCrossAxisMax = queryB,
+ queryCrossAxisMin = queryT,
+ crossAxisMax = b,
+ crossAxisMin = t,
+ )
+ AxisSouth ->
+ distanceScoreAlongAxis(
+ distanceMin = t,
+ distanceMax = queryB,
+ queryCrossAxisMax = queryR,
+ queryCrossAxisMin = queryL,
+ crossAxisMax = r,
+ crossAxisMin = l,
+ )
+ AxisWest ->
+ distanceScoreAlongAxis(
+ distanceMin = queryL,
+ distanceMax = r,
+ queryCrossAxisMax = queryB,
+ queryCrossAxisMin = queryT,
+ crossAxisMax = b,
+ crossAxisMin = t,
+ )
+ else -> Int.MAX_VALUE
+ }
+}
+
+/**
+ * This function will return a "score" of a rectangle relative to a query. A negative score means
+ * that the rectangle should be ignored, and a low score means that
+ */
+internal fun distanceScoreAlongAxis(
+ distanceMin: Int,
+ distanceMax: Int,
+ queryCrossAxisMax: Int,
+ queryCrossAxisMin: Int,
+ crossAxisMax: Int,
+ crossAxisMin: Int,
+): Int {
+ // small positive means it is close to the right, negative means there is overlap or it is to
+ // the left, which we will reject. We want small and positive.
+ val distanceAlongAxis = distanceMin - distanceMax
+ val maxOverlapPossible = queryCrossAxisMax - queryCrossAxisMin
+ // 0 with full overlap, increasingly large negative numbers without
+ val overlap =
+ maxOverlapPossible + max(queryCrossAxisMin, crossAxisMin) -
+ min(queryCrossAxisMax, crossAxisMax)
+
+ return (distanceAlongAxis + 1) * (overlap + 1)
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectManager.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectManager.kt
new file mode 100644
index 0000000..63751ce
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectManager.kt
@@ -0,0 +1,355 @@
+/*
+ * 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.spatial
+
+import androidx.collection.mutableObjectListOf
+import androidx.compose.ui.ComposeUiFlags
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.geometry.MutableRect
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Matrix
+import androidx.compose.ui.graphics.isIdentity
+import androidx.compose.ui.node.LayoutNode
+import androidx.compose.ui.node.NodeCoordinator
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.plus
+import androidx.compose.ui.unit.round
+import androidx.compose.ui.unit.toOffset
+
+internal class RectManager {
+
+ val rects: RectList = RectList()
+
+ private val callbacks = mutableObjectListOf<() -> Unit>()
+ private var isDirty = false
+ private var isFragmented = false
+
+ fun invalidate() {
+ isDirty = true
+ }
+
+ // TODO: we need to make sure these are dispatched after draw if needed
+ fun dispatchCallbacks() {
+ if (isDirty) {
+ isDirty = false
+ // The hierarchy is "settled" in terms of nodes being added/removed for this frame
+ // This makes it a reasonable time to "defragment" the RectList data structure. This
+ // will keep operations on this data structure efficient over time. This is a fairly
+ // cheap operation to run, so we just do it every time
+ if (isFragmented) {
+ isFragmented = false
+ rects.defragment()
+ }
+ callbacks.forEach { it() }
+ }
+ }
+
+ fun registerOnChangedCallback(callback: () -> Unit): Any? {
+ callbacks.add(callback)
+ return callback
+ }
+
+ fun unregisterOnChangedCallback(token: Any?) {
+ @Suppress("UNCHECKED_CAST")
+ token as? (() -> Unit) ?: return
+ callbacks.remove(token)
+ }
+
+ fun onLayoutLayerPositionalPropertiesChanged(layoutNode: LayoutNode) {
+ @OptIn(ExperimentalComposeUiApi::class) if (!ComposeUiFlags.isRectTrackingEnabled) return
+ val outerToInnerOffset = layoutNode.outerToInnerOffset()
+ if (outerToInnerOffset.isSet) {
+ // translational properties only. AARB still valid.
+ layoutNode.outerToInnerOffset = outerToInnerOffset
+ layoutNode.outerToInnerOffsetDirty = false
+ layoutNode.forEachChild {
+ // NOTE: this calls rectlist.move(...) so does not need to be recursive
+ onLayoutPositionChanged(it, it.outerCoordinator.position, false)
+ }
+ } else {
+ // there are rotations/skews/scales going on, so we need to do a more expensive update
+ insertOrUpdateTransformedNodeSubhierarchy(layoutNode)
+ }
+ }
+
+ fun onLayoutPositionChanged(
+ layoutNode: LayoutNode,
+ position: IntOffset,
+ firstPlacement: Boolean
+ ) {
+ @OptIn(ExperimentalComposeUiApi::class) if (!ComposeUiFlags.isRectTrackingEnabled) return
+ // Our goal here is to get the right "root" coordinates for every layout. We can use
+ // LayoutCoordinates.localToRoot to calculate this somewhat readily, however this function
+ // is getting called with a very high frequency and so it is important that extracting these
+ // coordinates remains relatively cheap to limit the overhead of this tracking. The
+ // LayoutCoordinates will traverse up the entire "spine" of the hierarchy, so as we do this
+ // calculation for many nodes, we would be making many redundant calculations. In order to
+ // minimize this, we store the "offsetFromRoot" of each layout node as we calculate it, and
+ // attempt to utilize this value when calculating it for a node that is below it.
+ // Additionally, we calculate and cache the parent's "outer to inner offset" which may
+ val delegate = layoutNode.measurePassDelegate
+ val width = delegate.measuredWidth
+ val height = delegate.measuredHeight
+
+ val parent = layoutNode.parent
+ val offset: IntOffset
+
+ val lastOffset = layoutNode.offsetFromRoot
+ val lastSize = layoutNode.lastSize
+ val lastWidth = lastSize.width
+ val lastHeight = lastSize.height
+
+ var hasNonTranslationTransformations = false
+
+ if (parent != null) {
+ val parentOffsetDirty = parent.outerToInnerOffsetDirty
+ val parentOffset = parent.offsetFromRoot
+ val prevOuterToInnerOffset = parent.outerToInnerOffset
+
+ offset =
+ if (parentOffset.isSet) {
+ val parentOuterInnerOffset =
+ if (parentOffsetDirty) {
+ val it = parent.outerToInnerOffset()
+
+ parent.outerToInnerOffset = it
+ parent.outerToInnerOffsetDirty = false
+ it
+ } else {
+ prevOuterToInnerOffset
+ }
+ hasNonTranslationTransformations = !parentOuterInnerOffset.isSet
+ parentOffset + parentOuterInnerOffset + position
+ } else {
+ layoutNode.outerCoordinator.positionInRoot()
+ }
+ } else {
+ // root
+ offset = position
+ }
+
+ // If unset is returned then that means there is a rotation/skew/scale
+ if (hasNonTranslationTransformations || !offset.isSet) {
+ insertOrUpdateTransformedNode(layoutNode, position, firstPlacement)
+ return
+ }
+
+ layoutNode.offsetFromRoot = offset
+ layoutNode.lastSize = IntSize(width, height)
+
+ val l = offset.x
+ val t = offset.y
+ val r = l + width
+ val b = t + height
+
+ if (!firstPlacement && offset == lastOffset && lastWidth == width && lastHeight == height) {
+ return
+ }
+
+ insertOrUpdate(layoutNode, firstPlacement, l, t, r, b)
+ }
+
+ private fun insertOrUpdateTransformedNodeSubhierarchy(layoutNode: LayoutNode) {
+ layoutNode.forEachChild {
+ insertOrUpdateTransformedNode(it, it.outerCoordinator.position, false)
+ insertOrUpdateTransformedNodeSubhierarchy(it)
+ }
+ }
+
+ private val cachedRect = MutableRect(0f, 0f, 0f, 0f)
+
+ private fun insertOrUpdateTransformedNode(
+ layoutNode: LayoutNode,
+ position: IntOffset,
+ firstPlacement: Boolean,
+ ) {
+ val coord = layoutNode.outerCoordinator
+ val delegate = layoutNode.measurePassDelegate
+ val width = delegate.measuredWidth
+ val height = delegate.measuredHeight
+ val rect = cachedRect
+
+ rect.set(
+ left = position.x.toFloat(),
+ top = position.y.toFloat(),
+ right = (position.x + width).toFloat(),
+ bottom = (position.y + height).toFloat(),
+ )
+
+ coord.boundingRectInRoot(rect)
+
+ val l = rect.left.toInt()
+ val t = rect.top.toInt()
+ val r = rect.right.toInt()
+ val b = rect.bottom.toInt()
+ val id = layoutNode.semanticsId
+ // NOTE: we call update here instead of move since the subhierarchy will not be moved by a
+ // simple delta since we are dealing with rotation/skew/scale/etc.
+ if (firstPlacement || !rects.update(id, l, t, r, b)) {
+ val parentId = layoutNode.parent?.semanticsId ?: -1
+ rects.insert(
+ id,
+ l,
+ t,
+ r,
+ b,
+ parentId = parentId,
+ )
+ }
+ invalidate()
+ }
+
+ private fun insertOrUpdate(
+ layoutNode: LayoutNode,
+ firstPlacement: Boolean,
+ l: Int,
+ t: Int,
+ r: Int,
+ b: Int,
+ ) {
+ val id = layoutNode.semanticsId
+ if (firstPlacement || !rects.move(id, l, t, r, b)) {
+ val parentId = layoutNode.parent?.semanticsId ?: -1
+ rects.insert(
+ id,
+ l,
+ t,
+ r,
+ b,
+ parentId = parentId,
+ )
+ }
+ invalidate()
+ }
+
+ private fun NodeCoordinator.positionInRoot(): IntOffset {
+ // TODO: can we use offsetFromRoot here to speed up calculation?
+ var position = Offset.Zero
+ var coordinator: NodeCoordinator? = this
+ while (coordinator != null) {
+ val layer = coordinator.layer
+ position += coordinator.position
+ coordinator = coordinator.wrappedBy
+ if (layer != null) {
+ val matrix = layer.underlyingMatrix
+ val analysis = matrix.analyzeComponents()
+ if (analysis == 0b11) continue
+ val hasNonTranslationComponents = analysis and 0b10 == 0
+ if (hasNonTranslationComponents) {
+ return IntOffset.Max
+ }
+ position = matrix.map(position)
+ }
+ }
+ return position.round()
+ }
+
+ private fun NodeCoordinator.boundingRectInRoot(rect: MutableRect) {
+ // TODO: can we use offsetFromRoot here to speed up calculation?
+ var coordinator: NodeCoordinator? = this
+ while (coordinator != null) {
+ val layer = coordinator.layer
+ rect.translate(coordinator.position.toOffset())
+ coordinator = coordinator.wrappedBy
+ if (layer != null) {
+ val matrix = layer.underlyingMatrix
+ if (!matrix.isIdentity()) {
+ matrix.map(rect)
+ }
+ }
+ }
+ }
+
+ private fun LayoutNode.outerToInnerOffset(): IntOffset {
+ val terminator = outerCoordinator
+ var position = Offset.Zero
+ var coordinator: NodeCoordinator? = innerCoordinator
+ while (coordinator != null) {
+ if (coordinator === terminator) break
+ val layer = coordinator.layer
+ position += coordinator.position
+ coordinator = coordinator.wrappedBy
+ if (layer != null) {
+ val matrix = layer.underlyingMatrix
+ val analysis = matrix.analyzeComponents()
+ if (analysis.isIdentity) continue
+ if (analysis.hasNonTranslationComponents) {
+ return IntOffset.Max
+ }
+ position = matrix.map(position)
+ }
+ }
+ return position.round()
+ }
+
+ fun remove(layoutNode: LayoutNode) {
+ rects.remove(layoutNode.semanticsId)
+ invalidate()
+ isFragmented = true
+ }
+}
+
+/**
+ * Returns true if the offset is not IntOffset.Max. In this class we are using `IntOffset.Max` to be
+ * a sentinel value for "unspecified" so that we can avoid boxing.
+ */
+private val IntOffset.isSet: Boolean
+ get() = this != IntOffset.Max
+
+/**
+ * We have logic that looks at whether or not a Matrix is an identity matrix, in which case we avoid
+ * doing expensive matrix calculations. Additionally, even if the matrix is non-identity, we can
+ * avoid a lot of extra work if the matrix is only doing translations, and no rotations/skews/scale.
+ *
+ * Since checking for these conditions involves a lot of overlapping work, we have this bespoke
+ * function which will return an Int that encodes the answer to both questions. If the 2nd bit of
+ * the result is set, this means that there are no rotations/skews/scales. If the first bit of the
+ * result is set, it means that there are no translations.
+ *
+ * This also means that the result of `0b11` indicates that it is the identity matrix.
+ */
+private fun Matrix.analyzeComponents(): Int {
+ // See top-level comment
+ val v = values
+ if (v.size < 16) return 0
+ val isIdentity3x3 =
+ v[0] == 1f &&
+ v[1] == 0f &&
+ v[2] == 0f &&
+ v[4] == 0f &&
+ v[5] == 1f &&
+ v[6] == 0f &&
+ v[8] == 0f &&
+ v[9] == 0f &&
+ v[10] == 1f
+
+ // translation components
+ val hasNoTranslationComponents = v[12] == 0f && v[13] == 0f && v[14] == 0f && v[15] == 1f
+
+ return isIdentity3x3.toInt() shl 1 or hasNoTranslationComponents.toInt()
+}
+
+@Suppress("NOTHING_TO_INLINE")
+private inline val Int.isIdentity: Boolean
+ get() = this == 0b11
+
+@Suppress("NOTHING_TO_INLINE")
+private inline val Int.hasNonTranslationComponents: Boolean
+ get() = this and 0b10 == 0
+
+@Suppress("NOTHING_TO_INLINE") private inline fun Boolean.toInt(): Int = if (this) 1 else 0
diff --git a/coordinatorlayout/coordinatorlayout/build.gradle b/coordinatorlayout/coordinatorlayout/build.gradle
index 8f27824..3f4475a 100644
--- a/coordinatorlayout/coordinatorlayout/build.gradle
+++ b/coordinatorlayout/coordinatorlayout/build.gradle
@@ -27,8 +27,8 @@
})
androidTestImplementation(libs.espressoCore, excludes.espresso)
androidTestImplementation(libs.bundles.espressoContrib, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(project(":internal-testutils-runtime"), {
exclude group: "androidx.coordinatorlayout", module: "coordinatorlayout"
})
diff --git a/core/core-google-shortcuts/build.gradle b/core/core-google-shortcuts/build.gradle
index 4824910..39be349 100644
--- a/core/core-google-shortcuts/build.gradle
+++ b/core/core-google-shortcuts/build.gradle
@@ -42,8 +42,8 @@
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore)
androidTestImplementation(libs.espressoIntents)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.truth)
androidTestImplementation(libs.junit)
}
diff --git a/core/core-graphics-integration-tests/testapp/build.gradle b/core/core-graphics-integration-tests/testapp/build.gradle
index 5ed44fb..21462c7 100644
--- a/core/core-graphics-integration-tests/testapp/build.gradle
+++ b/core/core-graphics-integration-tests/testapp/build.gradle
@@ -30,7 +30,7 @@
dependencies {
implementation(project(":core:core-ktx"))
- implementation(projectOrArtifact(":appcompat:appcompat"))
+ implementation(project(":appcompat:appcompat"))
implementation("androidx.annotation:annotation:1.8.1")
compileOnly(project(":annotation:annotation-sampled"))
}
diff --git a/core/core-splashscreen/build.gradle b/core/core-splashscreen/build.gradle
index 58806d7..c2c50ee 100644
--- a/core/core-splashscreen/build.gradle
+++ b/core/core-splashscreen/build.gradle
@@ -45,7 +45,7 @@
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.testUiautomator)
androidTestImplementation(libs.truth)
- androidTestImplementation(projectOrArtifact(":appcompat:appcompat"))
+ androidTestImplementation(project(":appcompat:appcompat"))
androidTestImplementation(project(":test:screenshot:screenshot"))
}
diff --git a/core/core-splashscreen/samples/build.gradle b/core/core-splashscreen/samples/build.gradle
index 404af32..09a3102 100644
--- a/core/core-splashscreen/samples/build.gradle
+++ b/core/core-splashscreen/samples/build.gradle
@@ -41,7 +41,7 @@
dependencies {
implementation(project(":core:core-splashscreen"))
implementation(project(":core:core-ktx"))
- implementation(projectOrArtifact(":appcompat:appcompat"))
+ implementation(project(":appcompat:appcompat"))
implementation("androidx.annotation:annotation:1.8.1")
- implementation(projectOrArtifact(":interpolator:interpolator"))
+ implementation(project(":interpolator:interpolator"))
}
diff --git a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestController.kt b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestController.kt
index 0e0e9b1..992f588 100644
--- a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestController.kt
+++ b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestController.kt
@@ -44,11 +44,9 @@
/**
* If set to true, takes a screenshot of the splash screen and saves it in
- * [SplashScreenTestController.splashScreenScreenshot] and a second screenshot of
- * [androidx.core.splashscreen.SplashScreenViewProvider.view] and saves it in
- * [SplashScreenTestController.splashScreenViewScreenShot]
+ * [SplashScreenTestController.splashScreenScreenshot]
*/
-internal const val EXTRA_SPLASHSCREEN_VIEW_SCREENSHOT = "SplashScreenViewScreenShot"
+internal const val EXTRA_SPLASHSCREEN_SCREENSHOT = "SplashScreenScreenShot"
public interface SplashScreenTestControllerHolder {
public var controller: SplashScreenTestController
@@ -56,7 +54,6 @@
public class SplashScreenTestController(internal val activity: Activity) {
- public var splashScreenViewScreenShot: Bitmap? = null
public var splashScreenScreenshot: Bitmap? = null
public var splashscreenIconId: Int = 0
public var splashscreenBackgroundId: Int = 0
@@ -95,7 +92,7 @@
val extras = intent.extras ?: Bundle.EMPTY
val useListener = extras.getBoolean(EXTRA_ANIMATION_LISTENER)
- val takeScreenShot = extras.getBoolean(EXTRA_SPLASHSCREEN_VIEW_SCREENSHOT)
+ val takeScreenShot = extras.getBoolean(EXTRA_SPLASHSCREEN_SCREENSHOT)
val waitForSplashscreen = extras.getBoolean(EXTRA_SPLASHSCREEN_WAIT)
val tv = TypedValue()
@@ -137,17 +134,7 @@
if (onExitAnimationListener(splashScreenViewProvider)) {
return@setOnExitAnimationListener
}
- if (takeScreenShot) {
- splashScreenViewProvider.view.postDelayed(
- {
- splashScreenViewScreenShot =
- getInstrumentation().uiAutomation.takeScreenshot()
- splashScreenViewProvider.remove()
- exitAnimationListenerLatch.countDown()
- },
- 100
- )
- } else {
+ if (!takeScreenShot) {
splashScreenViewProvider.remove()
exitAnimationListenerLatch.countDown()
}
diff --git a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt
index 74c7f09..96aa766 100644
--- a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt
+++ b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt
@@ -19,6 +19,7 @@
import android.content.Intent
import android.graphics.Bitmap
import android.os.Bundle
+import android.os.SystemClock
import android.util.Base64
import android.util.Log
import android.view.View
@@ -32,6 +33,7 @@
import androidx.test.uiautomator.UiDevice
import java.io.ByteArrayOutputStream
import java.io.File
+import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.util.concurrent.CountDownLatch
@@ -144,18 +146,68 @@
@SdkSuppress(minSdkVersion = 23)
@Test
public fun splashscreenViewScreenshotComparison() {
- val activity = startActivityWithSplashScreen {
+ val controller = startActivityWithSplashScreen {
// Clear out any previous instances
it.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
it.putExtra(EXTRA_SPLASHSCREEN_WAIT, true)
it.putExtra(EXTRA_ANIMATION_LISTENER, true)
- it.putExtra(EXTRA_SPLASHSCREEN_VIEW_SCREENSHOT, true)
+ it.putExtra(EXTRA_SPLASHSCREEN_SCREENSHOT, true)
}
- assertTrue(activity.waitedLatch.await(2, TimeUnit.SECONDS))
- activity.waitBarrier.set(false)
- activity.exitAnimationListenerLatch.await(2, TimeUnit.SECONDS)
- compareBitmaps(activity.splashScreenScreenshot!!, activity.splashScreenViewScreenShot!!)
+ var splashScreenViewScreenShot: Bitmap? = null
+
+ controller.doOnExitAnimation {
+ // b/355716686
+ // During the transition from the splash screen of system starting window to the
+ // activity, there may be a moment that `PhoneWindowManager`'s
+ // `mTopFullscreenOpaqueWindowState` would be `null`, which might lead to the flicker of
+ // status bar (b/64291272, ag/2664318)
+ val topFullscreenWinState = "mTopFullscreenOpaqueWindowState"
+
+ // We should take the screenshot when `mTopFullscreenOpaqueWindowState` is window of the
+ // activity
+ val topFullscreenWinStateBelongsToActivity =
+ Regex(
+ topFullscreenWinState +
+ "=Window\\{.*" +
+ controller.activity.componentName.className +
+ "\\}"
+ )
+
+ val isTopFullscreenWinStateReady: () -> Boolean = {
+ val dumpedWindowPolicy =
+ InstrumentationRegistry.getInstrumentation()
+ .uiAutomation
+ .executeShellCommand("dumpsys window p")
+ .use { FileInputStream(it.fileDescriptor).reader().readText() }
+
+ !dumpedWindowPolicy.contains(topFullscreenWinState) ||
+ dumpedWindowPolicy.contains(topFullscreenWinStateBelongsToActivity)
+ }
+
+ val timeout = 2000L
+ val interval = 100L
+ val start = SystemClock.uptimeMillis()
+ var topFullscreenWinStateReady = isTopFullscreenWinStateReady()
+ while (!topFullscreenWinStateReady && SystemClock.uptimeMillis() - start < timeout) {
+ SystemClock.sleep(interval)
+ topFullscreenWinStateReady = isTopFullscreenWinStateReady()
+ }
+ if (!topFullscreenWinStateReady)
+ fail("$topFullscreenWinState is not ready, cannot take screenshot")
+
+ splashScreenViewScreenShot =
+ InstrumentationRegistry.getInstrumentation().uiAutomation.takeScreenshot()
+ it.remove()
+ controller.exitAnimationListenerLatch.countDown()
+ true
+ }
+
+ assertTrue(controller.waitedLatch.await(2, TimeUnit.SECONDS))
+ controller.waitBarrier.set(false)
+ controller.exitAnimationListenerLatch.await(2, TimeUnit.SECONDS)
+
+ compareBitmaps(controller.splashScreenScreenshot!!, splashScreenViewScreenShot!!)
}
/**
diff --git a/core/core/build.gradle b/core/core/build.gradle
index bae3ce7..d4d7840 100644
--- a/core/core/build.gradle
+++ b/core/core/build.gradle
@@ -33,7 +33,7 @@
api(libs.kotlinStdlib)
// We don't ship this as a public artifact, so it must remain a project-type dependency.
- annotationProcessor(projectOrArtifact(":versionedparcelable:versionedparcelable-compiler"))
+ annotationProcessor(project(":versionedparcelable:versionedparcelable-compiler"))
androidTestImplementation(libs.junit)
androidTestImplementation(libs.kotlinStdlib)
@@ -43,7 +43,7 @@
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.truth)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
androidTestImplementation(libs.testUiautomator)
androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.6.2")
@@ -51,8 +51,8 @@
// Including both dexmakers allows support for all API levels plus final mocking support on
// API 28+. The implementation is swapped based on the finality of the mock type. This
// delegation is handled manually inside androidx.core.util.mockito.CustomMockMaker.
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockitoInline, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.dexmakerMockito)
+ androidTestImplementation(libs.dexmakerMockitoInline)
androidTestImplementation("androidx.appcompat:appcompat:1.1.0") {
exclude group: "androidx.core", module: "core"
}
diff --git a/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java b/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java
index ed0ed4d..007491b 100644
--- a/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java
+++ b/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java
@@ -37,6 +37,7 @@
import androidx.test.filters.MediumTest;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -365,6 +366,7 @@
*/
@MediumTest
@Test
+ @Ignore("JobIntentService is deprecated and no longer maintained")
public void testEnqueueOne() throws Throwable {
initStatics();
@@ -386,6 +388,7 @@
*/
@MediumTest
@Test
+ @Ignore("JobIntentService is deprecated and no longer maintained")
public void testEnqueueMultiple() throws Throwable {
initStatics();
@@ -410,6 +413,7 @@
*/
@MediumTest
@Test
+ @Ignore("JobIntentService is deprecated and no longer maintained")
public void testEnqueueSubWork() throws Throwable {
initStatics();
@@ -439,6 +443,7 @@
*/
@MediumTest
@Test
+ @Ignore("JobIntentService is deprecated and no longer maintained")
@RequiresApi(26)
public void testStopWhileWorking() throws Throwable {
if (Build.VERSION.SDK_INT < 26) {
diff --git a/core/core/src/main/java/androidx/core/content/FileProvider.java b/core/core/src/main/java/androidx/core/content/FileProvider.java
index 2dfb577..d28602c 100644
--- a/core/core/src/main/java/androidx/core/content/FileProvider.java
+++ b/core/core/src/main/java/androidx/core/content/FileProvider.java
@@ -95,12 +95,11 @@
* <p>
* <b>Defining a FileProvider</b>
* <p>
- * Extend FileProvider with a default constructor, and call super with an XML resource file that
- * specifies the available files (see below for the structure of the XML file):
+ * Extend FileProvider with a default constructor:
* <pre class="prettyprint">
* public class MyFileProvider extends FileProvider {
* public MyFileProvider() {
- * super(R.xml.file_paths)
+ * ...
* }
* }
* </pre>
diff --git a/core/haptics/haptics/build.gradle b/core/haptics/haptics/build.gradle
index 044ca4b..5d5ca3c 100644
--- a/core/haptics/haptics/build.gradle
+++ b/core/haptics/haptics/build.gradle
@@ -34,8 +34,8 @@
api(libs.kotlinStdlib)
implementation("androidx.annotation:annotation:1.8.1")
- implementation(projectOrArtifact(":core:core"))
- implementation(projectOrArtifact(":media:media"))
+ implementation(project(":core:core"))
+ implementation(project(":media:media"))
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
diff --git a/datastore/datastore-compose-samples/build.gradle b/datastore/datastore-compose-samples/build.gradle
index 07d5fe6..69047af 100644
--- a/datastore/datastore-compose-samples/build.gradle
+++ b/datastore/datastore-compose-samples/build.gradle
@@ -33,19 +33,19 @@
}
dependencies {
- compileOnly(projectOrArtifact(":datastore:datastore-preferences-external-protobuf"))
+ compileOnly(project(":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')
- implementation(projectOrArtifact(":compose:ui:ui"))
- implementation(projectOrArtifact(":compose:ui:ui-tooling-preview"))
- implementation(projectOrArtifact(":compose:material:material"))
+ implementation(project(":compose:ui:ui"))
+ implementation(project(":compose:ui:ui-tooling-preview"))
+ implementation(project(":compose:material:material"))
testImplementation('junit:junit:4.13.2')
- debugImplementation(projectOrArtifact(":compose:ui:ui-tooling"))
- debugImplementation(projectOrArtifact(":compose:ui:ui-test-manifest"))
+ debugImplementation(project(":compose:ui:ui-tooling"))
+ debugImplementation(project(":compose:ui:ui-test-manifest"))
implementation("androidx.datastore:datastore-preferences:1.0.0")
implementation("com.google.protobuf:protobuf-javalite:3.19.4")
diff --git a/development/update_studio.sh b/development/update_studio.sh
index cdd888e..3fa7ebb 100755
--- a/development/update_studio.sh
+++ b/development/update_studio.sh
@@ -48,7 +48,7 @@
sed -i "s/androidStudio = .*/androidStudio = \"$STUDIO_VERSION\"/g" gradle/libs.versions.toml
# update settings.gradle
-sed -i "s/com.android.settings:com.android.settings.gradle.plugin:.*/com.android.settings:com.android.settings.gradle.plugin:$AGP_VERSION\")/g" settings.gradle
+sed -i "s/com.android.settings:com.android.settings.gradle.plugin:[0-9a-z\.\-]*/com.android.settings:com.android.settings.gradle.plugin:$AGP_VERSION\")/g" settings.gradle
# Pull all UTP artifacts for ADT version
ADT_VERSION=${3:-$LINT_VERSION}
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index e89eb38..55d5605 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -63,6 +63,7 @@
docs(project(":camera:camera-feature-combination-query-play-services"))
docs(project(":camera:camera-lifecycle"))
samples(project(":camera:camera-lifecycle:camera-lifecycle-samples"))
+ docs(project(":camera:camera-media3-effect"))
docs(project(":camera:camera-mlkit-vision"))
docs(project(":camera:camera-testing"))
docs(project(":camera:camera-video"))
diff --git a/draganddrop/draganddrop/build.gradle b/draganddrop/draganddrop/build.gradle
index f58b809..089c9e8 100644
--- a/draganddrop/draganddrop/build.gradle
+++ b/draganddrop/draganddrop/build.gradle
@@ -33,8 +33,8 @@
api("androidx.appcompat:appcompat:1.4.0")
api("androidx.core:core:1.7.0")
annotationProcessor(libs.nullaway)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
diff --git a/dynamicanimation/dynamicanimation-ktx/build.gradle b/dynamicanimation/dynamicanimation-ktx/build.gradle
index f7e8e34..fbfd7c9 100644
--- a/dynamicanimation/dynamicanimation-ktx/build.gradle
+++ b/dynamicanimation/dynamicanimation-ktx/build.gradle
@@ -39,8 +39,8 @@
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
}
androidx {
diff --git a/dynamicanimation/dynamicanimation/build.gradle b/dynamicanimation/dynamicanimation/build.gradle
index 26594fb..aba2b5a 100644
--- a/dynamicanimation/dynamicanimation/build.gradle
+++ b/dynamicanimation/dynamicanimation/build.gradle
@@ -21,8 +21,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
}
androidx {
diff --git a/emoji/emoji/build.gradle b/emoji/emoji/build.gradle
index 83a20ed..40bfe37 100644
--- a/emoji/emoji/build.gradle
+++ b/emoji/emoji/build.gradle
@@ -35,8 +35,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(project(":internal-testutils-runtime"))
}
diff --git a/emoji2/emoji2-benchmark/build.gradle b/emoji2/emoji2-benchmark/build.gradle
index a7db4fa..b5c2de3 100644
--- a/emoji2/emoji2-benchmark/build.gradle
+++ b/emoji2/emoji2-benchmark/build.gradle
@@ -49,14 +49,14 @@
dependencies {
androidTestImplementation(project(":emoji2:emoji2"))
androidTestImplementation(project(":emoji2:emoji2-bundled"))
- androidTestImplementation(projectOrArtifact(":benchmark:benchmark-junit4"))
+ androidTestImplementation(project(":benchmark:benchmark-junit4"))
androidTestImplementation(libs.junit)
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation project(':internal-testutils-runtime')
androidTestImplementation(libs.kotlinStdlib)
}
diff --git a/emoji2/emoji2-bundled/build.gradle b/emoji2/emoji2-bundled/build.gradle
index d48ac83..df76592 100644
--- a/emoji2/emoji2-bundled/build.gradle
+++ b/emoji2/emoji2-bundled/build.gradle
@@ -44,8 +44,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation project(':internal-testutils-runtime')
// view tests that use font are in this module as well; for licensing reasons
diff --git a/emoji2/emoji2-views-helper/build.gradle b/emoji2/emoji2-views-helper/build.gradle
index 629fc9f..1362d13 100644
--- a/emoji2/emoji2-views-helper/build.gradle
+++ b/emoji2/emoji2-views-helper/build.gradle
@@ -24,8 +24,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation project(':internal-testutils-runtime')
}
diff --git a/emoji2/emoji2-views/build.gradle b/emoji2/emoji2-views/build.gradle
index 41926fe..9e6c338 100644
--- a/emoji2/emoji2-views/build.gradle
+++ b/emoji2/emoji2-views/build.gradle
@@ -24,8 +24,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation project(':internal-testutils-runtime')
}
diff --git a/emoji2/emoji2/build.gradle b/emoji2/emoji2/build.gradle
index 182c191..a4597af 100644
--- a/emoji2/emoji2/build.gradle
+++ b/emoji2/emoji2/build.gradle
@@ -38,8 +38,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation project(':internal-testutils-runtime')
}
diff --git a/fragment/fragment-testing/build.gradle b/fragment/fragment-testing/build.gradle
index e782228..6e83dc1 100644
--- a/fragment/fragment-testing/build.gradle
+++ b/fragment/fragment-testing/build.gradle
@@ -46,8 +46,8 @@
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.truth)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
lintPublish(project(":fragment:fragment-testing-lint"))
}
diff --git a/fragment/fragment/build.gradle b/fragment/fragment/build.gradle
index 2d7e64a..eff0092 100644
--- a/fragment/fragment/build.gradle
+++ b/fragment/fragment/build.gradle
@@ -61,13 +61,13 @@
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.truth)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(project(":internal-testutils-runtime"), {
exclude group: "androidx.fragment", module: "fragment"
})
- testImplementation(projectOrArtifact(":fragment:fragment"))
+ testImplementation(project(":fragment:fragment"))
testImplementation(libs.kotlinStdlib)
testImplementation(libs.junit)
testRuntimeOnly(libs.testCore)
diff --git a/fragment/integration-tests/testapp/build.gradle b/fragment/integration-tests/testapp/build.gradle
index 364175d..5447bc6 100644
--- a/fragment/integration-tests/testapp/build.gradle
+++ b/fragment/integration-tests/testapp/build.gradle
@@ -30,7 +30,7 @@
implementation("androidx.core:core-ktx:1.13.0")
implementation(project(":fragment:fragment-ktx"))
implementation("androidx.lifecycle:lifecycle-viewmodel:2.6.2")
- implementation(projectOrArtifact(":transition:transition"))
+ implementation(project(":transition:transition"))
implementation("androidx.recyclerview:recyclerview:1.1.0")
debugImplementation(project(":fragment:fragment-testing-manifest"))
diff --git a/glance/glance-appwidget-testing/build.gradle b/glance/glance-appwidget-testing/build.gradle
index f8a6763..8b6c34a 100644
--- a/glance/glance-appwidget-testing/build.gradle
+++ b/glance/glance-appwidget-testing/build.gradle
@@ -69,5 +69,5 @@
inceptionYear = "2023"
description = "This library provides APIs for developers to use for testing their appWidget specific Glance composables."
legacyDisableKotlinStrictApiMode = true
- samples(projectOrArtifact(":glance:glance-appwidget-testing:glance-appwidget-testing-samples"))
+ samples(project(":glance:glance-appwidget-testing:glance-appwidget-testing-samples"))
}
diff --git a/glance/glance-appwidget/build.gradle b/glance/glance-appwidget/build.gradle
index 7558274..5357000 100644
--- a/glance/glance-appwidget/build.gradle
+++ b/glance/glance-appwidget/build.gradle
@@ -116,7 +116,7 @@
"using a Jetpack Compose-style API."
legacyDisableKotlinStrictApiMode = true
metalavaK2UastEnabled = false
- samples(projectOrArtifact(":glance:glance-appwidget:glance-appwidget-samples"))
+ samples(project(":glance:glance-appwidget:glance-appwidget-samples"))
}
LayoutGeneratorTask.registerLayoutGenerator(
diff --git a/glance/glance-appwidget/src/androidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt b/glance/glance-appwidget/src/androidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
index 2bb9ac2..a3d01ba 100644
--- a/glance/glance-appwidget/src/androidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
+++ b/glance/glance-appwidget/src/androidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
@@ -49,6 +49,7 @@
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
+import androidx.datastore.dataStoreFile
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
@@ -644,7 +645,6 @@
@Test
fun layoutConfigurationCanBeDeleted() {
- val fakeIndex = 9999
TestGlanceAppWidget.uiDefinition = { Text("something") }
mHostRule.startHost()
@@ -655,8 +655,7 @@
}
val appWidgetId = (glanceId as AppWidgetId).appWidgetId
- val config = LayoutConfiguration.create(context, appWidgetId, nextIndex = fakeIndex)
- val file = config.dataStoreFile
+ val file = context.dataStoreFile(layoutDatastoreKey(appWidgetId))
assertThat(file.exists())
val isDeleted = LayoutConfiguration.delete(context, glanceId)
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/WidgetLayout.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/WidgetLayout.kt
index d1595e4..8574c68 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/WidgetLayout.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/WidgetLayout.kt
@@ -86,9 +86,6 @@
private val existingLayoutIds: MutableSet<Int> = mutableSetOf(),
) {
- @VisibleForTesting
- internal val dataStoreFile = context.dataStoreFile(layoutDatastoreKey(appWidgetId))
-
internal companion object {
/** Creates a [LayoutConfiguration] retrieving known layouts from file, if they exist. */
@@ -300,7 +297,8 @@
private val GlanceModifier.heightModifier: Dimension
get() = findModifier<HeightModifier>()?.height ?: Dimension.Wrap
-private fun layoutDatastoreKey(appWidgetId: Int): String = "appWidgetLayout-$appWidgetId"
+@VisibleForTesting
+internal fun layoutDatastoreKey(appWidgetId: Int): String = "appWidgetLayout-$appWidgetId"
private object LayoutStateDefinition : GlanceStateDefinition<LayoutProto.LayoutConfig> {
override fun getLocation(context: Context, fileKey: String): File =
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 99e5b63..932cb8d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -56,7 +56,7 @@
ksp = "2.0.10-1.0.24"
ktfmt = "0.50"
leakcanary = "2.13"
-media3 = "1.1.0"
+media3 = "1.4.1"
metalava = "1.0.0-alpha12"
mockito = "2.25.0"
moshi = "1.13.0"
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 3cbe2f3..e228db6 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -14,6 +14,8 @@
<trust group="androidx[.]annotation" version="1\.[0-7]\..*" regex="true" reason="Old versions, before signing"/>
<trust group="androidx[.]annotation" version="1\.9\.0-alpha[0-9][2-9]" regex="true" reason="New versions, not yet signed"/>
<trust group="androidx[.]collection" version="1\.[0-3]\..*" regex="true" reason="Old versions, before signing"/>
+ <trust group="androidx[.]collection" version="1\.5\.0-alpha[0-9][2-9]" regex="true" reason="Old versions, before signing"/>
+
<trust group="com.android.ndk.thirdparty" reason="b/215430394"/>
<trust group="com.android.tools" name="desugar_jdk_libs" reason="b/215430394"/>
<trust group="com.android.tools" name="desugar_jdk_libs_configuration" reason="b/215430394"/>
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/CanvasFrontBufferedRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/CanvasFrontBufferedRenderer.kt
index 598c67e..2110acd 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/CanvasFrontBufferedRenderer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/CanvasFrontBufferedRenderer.kt
@@ -70,13 +70,15 @@
@JvmOverloads
constructor(
surfaceView: SurfaceView,
- private val callback: Callback<T>,
+ callback: Callback<T>,
@HardwareBufferFormat val bufferFormat: Int = HardwareBuffer.RGBA_8888
) {
/** Target SurfaceView for rendering */
private var mSurfaceView: SurfaceView? = null
+ private var mCallback: Callback<T>? = null
+
/**
* Executor used to deliver callbacks for rendering as well as issuing surface control
* transactions
@@ -185,6 +187,7 @@
init {
mSurfaceView = surfaceView
+ mCallback = callback
surfaceView.holder.addCallback(mHolderCallback)
with(surfaceView.holder) {
if (surface != null && surface.isValid) {
@@ -253,7 +256,7 @@
}
canvas.drawColor(Color.BLACK, BlendMode.CLEAR)
}
- callback.onDrawFrontBufferedLayer(canvas, width, height, param)
+ mCallback?.onDrawFrontBufferedLayer(canvas, width, height, param)
}
@SuppressLint("WrongConstant")
@@ -293,7 +296,7 @@
transformHint
)
}
- callback.onFrontBufferedLayerRenderComplete(
+ mCallback?.onFrontBufferedLayerRenderComplete(
frontBufferSurfaceControl,
transaction
)
@@ -460,7 +463,7 @@
if (transform != BufferTransformHintResolver.UNKNOWN_TRANSFORM) {
transaction.setBufferTransform(parentSurfaceControl, transform)
}
- callback.onMultiBufferedLayerRenderComplete(
+ mCallback?.onMultiBufferedLayerRenderComplete(
frontBufferSurfaceControl,
parentSurfaceControl,
transaction
@@ -566,7 +569,7 @@
with(multiBufferedRenderer) {
mMultiBufferedRenderNode?.let { renderNode ->
val canvas = renderNode.beginRecording()
- callback.onDrawMultiBufferedLayer(canvas, width, height, params)
+ mCallback?.onDrawMultiBufferedLayer(canvas, width, height, params)
renderNode.endRecording()
}
@@ -671,6 +674,7 @@
mSurfaceView?.holder?.removeCallback(mHolderCallback)
mSurfaceView = null
releaseInternal(cancelPending) {
+ mCallback = null
onReleaseComplete?.invoke()
mHandlerThread.quit()
}
diff --git a/health/connect/connect-client/api/current.txt b/health/connect/connect-client/api/current.txt
index d5d6059..dbdb631 100644
--- a/health/connect/connect-client/api/current.txt
+++ b/health/connect/connect-client/api/current.txt
@@ -159,6 +159,7 @@
method public static String getReadPermission(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType);
method public static String getWritePermission(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType);
field public static final androidx.health.connect.client.permission.HealthPermission.Companion Companion;
+ field public static final String PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND = "android.permission.health.READ_HEALTH_DATA_IN_BACKGROUND";
field public static final String PERMISSION_WRITE_EXERCISE_ROUTE = "android.permission.health.WRITE_EXERCISE_ROUTE";
}
diff --git a/health/connect/connect-client/api/restricted_current.txt b/health/connect/connect-client/api/restricted_current.txt
index 518453c..eea62f1 100644
--- a/health/connect/connect-client/api/restricted_current.txt
+++ b/health/connect/connect-client/api/restricted_current.txt
@@ -159,6 +159,7 @@
method public static String getReadPermission(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType);
method public static String getWritePermission(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType);
field public static final androidx.health.connect.client.permission.HealthPermission.Companion Companion;
+ field public static final String PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND = "android.permission.health.READ_HEALTH_DATA_IN_BACKGROUND";
field public static final String PERMISSION_WRITE_EXERCISE_ROUTE = "android.permission.health.WRITE_EXERCISE_ROUTE";
}
diff --git a/health/connect/connect-client/build.gradle b/health/connect/connect-client/build.gradle
index 9b1779f..b212af5 100644
--- a/health/connect/connect-client/build.gradle
+++ b/health/connect/connect-client/build.gradle
@@ -77,8 +77,7 @@
}
testOptions.unitTests.includeAndroidResources = true
namespace "androidx.health.connect.client"
- compileSdk = 34
- compileSdkExtension = 10
+ compileSdk = 35
// TODO(b/352609562): Typedef with `toLong()`
experimentalProperties["android.lint.useK2Uast"] = false
}
diff --git a/health/connect/connect-client/samples/build.gradle b/health/connect/connect-client/samples/build.gradle
index 08b7839..fb7bcef 100644
--- a/health/connect/connect-client/samples/build.gradle
+++ b/health/connect/connect-client/samples/build.gradle
@@ -51,4 +51,5 @@
defaultConfig {
minSdkVersion 26
}
+ compileSdk = 35
}
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt
index 14f303c..ee854d9 100644
--- a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt
@@ -95,7 +95,7 @@
context.packageName,
PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong())
)
- .requestedPermissions
+ .requestedPermissions!!
.filter { it.startsWith(PERMISSION_PREFIX) }
.toTypedArray()
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/aggregate/HealthConnectClientAggregationExtensionsTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/aggregate/HealthConnectClientAggregationExtensionsTest.kt
index 05fe60c..653ea0c 100644
--- a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/aggregate/HealthConnectClientAggregationExtensionsTest.kt
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/aggregate/HealthConnectClientAggregationExtensionsTest.kt
@@ -78,7 +78,7 @@
context.packageName,
PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong())
)
- .requestedPermissions
+ .requestedPermissions!!
.filter { it.startsWith(PERMISSION_PREFIX) }
.toTypedArray()
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt
index d3b5712b..fc5bae4 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt
@@ -154,7 +154,6 @@
* @sample androidx.health.connect.client.samples.RequestBackgroundReadPermission
* @sample androidx.health.connect.client.samples.ReadRecordsInBackground
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY) // Hidden for now
const val PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND =
PERMISSION_PREFIX + "READ_HEALTH_DATA_IN_BACKGROUND"
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt
index a03c963..e38be6e 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt
@@ -312,7 +312,7 @@
@Suppress("Deprecation")
packageInfo.versionCode = versionCode
packageInfo.applicationInfo = ApplicationInfo()
- packageInfo.applicationInfo.enabled = enabled
+ packageInfo.applicationInfo!!.enabled = enabled
val packageManager = context.packageManager
Shadows.shadowOf(packageManager).installPackage(packageInfo)
}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/feature/HealthConnectApkImplTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/feature/HealthConnectApkImplTest.kt
index dea6c6b..47d6fb9 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/feature/HealthConnectApkImplTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/feature/HealthConnectApkImplTest.kt
@@ -121,7 +121,7 @@
@Suppress("Deprecation")
packageInfo.versionCode = versionCode
packageInfo.applicationInfo = ApplicationInfo()
- packageInfo.applicationInfo.enabled = true
+ packageInfo.applicationInfo!!.enabled = true
val packageManager = context.packageManager
Shadows.shadowOf(packageManager).installPackage(packageInfo)
}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/HealthConnectClientImplTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/HealthConnectClientImplTest.kt
index f1480f8..2bb9f0c 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/HealthConnectClientImplTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/HealthConnectClientImplTest.kt
@@ -841,7 +841,7 @@
val packageInfo = PackageInfo()
packageInfo.packageName = packageName
packageInfo.applicationInfo = ApplicationInfo()
- packageInfo.applicationInfo.enabled = enabled
+ packageInfo.applicationInfo!!.enabled = enabled
val packageManager = context.packageManager
Shadows.shadowOf(packageManager).installPackage(packageInfo)
}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClientTest.kt b/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClientTest.kt
index 609c83a5..ff2ea5c 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClientTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClientTest.kt
@@ -186,7 +186,7 @@
val packageInfo = PackageInfo()
packageInfo.packageName = packageName
packageInfo.applicationInfo = ApplicationInfo()
- packageInfo.applicationInfo.enabled = enabled
+ packageInfo.applicationInfo!!.enabled = enabled
val packageManager = context.packageManager
shadowOf(packageManager).installPackage(packageInfo)
}
diff --git a/health/connect/connect-testing/build.gradle b/health/connect/connect-testing/build.gradle
index 9439dd3..1c1c2b5 100644
--- a/health/connect/connect-testing/build.gradle
+++ b/health/connect/connect-testing/build.gradle
@@ -47,8 +47,7 @@
}
namespace "androidx.health.connect.testing"
testOptions.unitTests.includeAndroidResources = true
- compileSdk = 34
- compileSdkExtension = 10
+ compileSdk = 35
}
androidx {
diff --git a/health/connect/connect-testing/samples/build.gradle b/health/connect/connect-testing/samples/build.gradle
index c93f7cc..a9cb0e5 100644
--- a/health/connect/connect-testing/samples/build.gradle
+++ b/health/connect/connect-testing/samples/build.gradle
@@ -51,6 +51,7 @@
defaultConfig {
minSdkVersion 26
}
+ compileSdk = 35
}
tasks.withType(KotlinCompile).configureEach {
diff --git a/hilt/hilt-navigation-compose/build.gradle b/hilt/hilt-navigation-compose/build.gradle
index 7a73f47..19b6acb 100644
--- a/hilt/hilt-navigation-compose/build.gradle
+++ b/hilt/hilt-navigation-compose/build.gradle
@@ -45,7 +45,7 @@
dependencies {
implementation(libs.kotlinStdlib)
- api projectOrArtifact(":hilt:hilt-navigation")
+ api project(":hilt:hilt-navigation")
api("androidx.compose.runtime:runtime:1.0.1")
api("androidx.compose.ui:ui:1.0.1")
api("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
@@ -59,13 +59,13 @@
androidTestImplementation(libs.hiltAndroid)
androidTestImplementation(libs.hiltAndroidTesting)
kspAndroidTest(libs.hiltCompiler)
- androidTestImplementation(projectOrArtifact(":compose:material:material"))
+ androidTestImplementation(project(":compose:material:material"))
androidTestImplementation(project(":compose:test-utils"))
- androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-common"))
- androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-common-java8"))
- androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-livedata-core"))
- androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-viewmodel"))
- androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-viewmodel-savedstate"))
+ androidTestImplementation(project(":lifecycle:lifecycle-common"))
+ androidTestImplementation(project(":lifecycle:lifecycle-common-java8"))
+ androidTestImplementation(project(":lifecycle:lifecycle-livedata-core"))
+ androidTestImplementation(project(":lifecycle:lifecycle-viewmodel"))
+ androidTestImplementation(project(":lifecycle:lifecycle-viewmodel-savedstate"))
}
hilt {
@@ -79,6 +79,6 @@
inceptionYear = "2021"
description = "Navigation Compose Hilt Integration"
legacyDisableKotlinStrictApiMode = true
- samples(projectOrArtifact(":hilt:hilt-navigation-compose-samples"))
+ samples(project(":hilt:hilt-navigation-compose-samples"))
kotlinTarget = KotlinTarget.KOTLIN_1_9
}
diff --git a/hilt/hilt-navigation-compose/samples/build.gradle b/hilt/hilt-navigation-compose/samples/build.gradle
index c59ff49..06d2f91 100644
--- a/hilt/hilt-navigation-compose/samples/build.gradle
+++ b/hilt/hilt-navigation-compose/samples/build.gradle
@@ -37,7 +37,7 @@
implementation(libs.kotlinStdlib)
compileOnly(project(":annotation:annotation-sampled"))
- implementation(projectOrArtifact(":hilt:hilt-navigation-compose"))
+ implementation(project(":hilt:hilt-navigation-compose"))
}
androidx {
diff --git a/ink/ink-brush/api/current.txt b/ink/ink-brush/api/current.txt
index 7582fcf..7599e2b 100644
--- a/ink/ink-brush/api/current.txt
+++ b/ink/ink-brush/api/current.txt
@@ -57,6 +57,16 @@
public static final class BrushFamily.Companion {
}
+ public final class BrushUtil {
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush copyWithAndroidColor(androidx.ink.brush.Brush, android.graphics.Color color, optional androidx.ink.brush.BrushFamily family, optional float size, optional float epsilon);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static android.graphics.Color createAndroidColor(androidx.ink.brush.Brush);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder createBuilderWithAndroidColor(android.graphics.Color color);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush createWithAndroidColor(androidx.ink.brush.Brush.Companion, androidx.ink.brush.BrushFamily family, android.graphics.Color color, float size, float epsilon);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush createWithAndroidColor(androidx.ink.brush.BrushFamily family, android.graphics.Color color, float size, float epsilon);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder setAndroidColor(androidx.ink.brush.Brush.Builder, android.graphics.Color color);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder toBuilderWithAndroidColor(androidx.ink.brush.Brush, android.graphics.Color color);
+ }
+
@SuppressCompatibility @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.TYPEALIAS}) public @interface ExperimentalInkCustomBrushApi {
}
diff --git a/ink/ink-brush/api/restricted_current.txt b/ink/ink-brush/api/restricted_current.txt
index 7582fcf..7599e2b 100644
--- a/ink/ink-brush/api/restricted_current.txt
+++ b/ink/ink-brush/api/restricted_current.txt
@@ -57,6 +57,16 @@
public static final class BrushFamily.Companion {
}
+ public final class BrushUtil {
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush copyWithAndroidColor(androidx.ink.brush.Brush, android.graphics.Color color, optional androidx.ink.brush.BrushFamily family, optional float size, optional float epsilon);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static android.graphics.Color createAndroidColor(androidx.ink.brush.Brush);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder createBuilderWithAndroidColor(android.graphics.Color color);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush createWithAndroidColor(androidx.ink.brush.Brush.Companion, androidx.ink.brush.BrushFamily family, android.graphics.Color color, float size, float epsilon);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush createWithAndroidColor(androidx.ink.brush.BrushFamily family, android.graphics.Color color, float size, float epsilon);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder setAndroidColor(androidx.ink.brush.Brush.Builder, android.graphics.Color color);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder toBuilderWithAndroidColor(androidx.ink.brush.Brush, android.graphics.Color color);
+ }
+
@SuppressCompatibility @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.TYPEALIAS}) public @interface ExperimentalInkCustomBrushApi {
}
diff --git a/ink/ink-brush/build.gradle b/ink/ink-brush/build.gradle
index 2483290..353b80f 100644
--- a/ink/ink-brush/build.gradle
+++ b/ink/ink-brush/build.gradle
@@ -50,9 +50,9 @@
jvmAndroidMain {
dependsOn(commonMain)
dependencies {
- implementation(libs.kotlinStdlib)
api(libs.androidx.annotation)
- implementation(project(":collection:collection"))
+ implementation(libs.kotlinStdlib)
+ implementation("androidx.collection:collection:1.4.3")
implementation(project(":ink:ink-geometry"))
implementation(project(":ink:ink-nativeloader"))
}
@@ -100,5 +100,4 @@
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2024"
description = "Define brushes for freehand input."
- metalavaK2UastEnabled = false
}
diff --git a/ink/ink-brush/src/androidInstrumentedTest/kotlin/androidx/ink/brush/BrushExtensionsTest.kt b/ink/ink-brush/src/androidInstrumentedTest/kotlin/androidx/ink/brush/BrushExtensionsTest.kt
index b23e689..1a277fe 100644
--- a/ink/ink-brush/src/androidInstrumentedTest/kotlin/androidx/ink/brush/BrushExtensionsTest.kt
+++ b/ink/ink-brush/src/androidInstrumentedTest/kotlin/androidx/ink/brush/BrushExtensionsTest.kt
@@ -41,7 +41,7 @@
private val testFamily = BrushFamily(uri = "/brush-family:pencil")
@Test
- fun brushGetAndroidColor_getsCorrectColor() {
+ fun brushCreateAndroidColor_getsCorrectColor() {
val brush = Brush.createWithColorLong(testFamily, testColorLong, 1f, 1f)
// Note that expectedColor is not necessarily the same as testColor, because of precision
@@ -50,7 +50,7 @@
// the
// color internally as a ColorLong anyway).
val expectedColor = AndroidColor.valueOf(testColorLong)
- assertThat(brush.getAndroidColor()).isEqualTo(expectedColor)
+ assertThat(brush.createAndroidColor()).isEqualTo(expectedColor)
}
@Test
@@ -97,7 +97,7 @@
}
@Test
- fun brushBuilderAndroidColor_setsColor() {
+ fun brushBuilderSetAndroidColor_setsColor() {
val brush =
Brush.builder()
.setFamily(testFamily)
@@ -110,7 +110,7 @@
}
@Test
- fun brushBuilderAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
+ fun brushBuilderSetAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
val brush =
Brush.builder()
@@ -126,13 +126,13 @@
}
@Test
- fun brushWithAndroidColor_createsBrushWithColor() {
+ fun brushCreateWithAndroidColor_createsBrushWithColor() {
val brush = Brush.createWithAndroidColor(testFamily, testColor, 1f, 1f)
assertThat(brush.colorLong).isEqualTo(testColorLong)
}
@Test
- fun brushWithAndroidColor_withUnsupportedColorSpace_createsBrushWithConvertedColor() {
+ fun brushCreateWithAndroidColor_withUnsupportedColorSpace_createsBrushWithConvertedColor() {
val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
val brush = Brush.createWithAndroidColor(testFamily, unsupportedColor, 1f, 1f)
@@ -142,21 +142,10 @@
}
@Test
- fun brushUtilGetAndroidColor_getsCorrectColor() {
- val brush = Brush.createWithColorLong(testFamily, testColorLong, 1f, 1f)
-
- // Note that expectedColor is not necessarily the same as testColor, because of precision
- // loss
- // when converting from testColor to testColorLong.
- val expectedColor = AndroidColor.valueOf(testColorLong)
- assertThat(BrushUtil.getAndroidColor(brush)).isEqualTo(expectedColor)
- }
-
- @Test
- fun brushUtilToBuilderWithAndroidColor_setsColor() {
+ fun brushToBuilderWithAndroidColor_setsColor() {
val brush = Brush.createWithColorIntArgb(testFamily, 0x4499bb66, 2f, 0.2f)
- val newBrush = BrushUtil.toBuilderWithAndroidColor(brush, testColor).build()
+ val newBrush = brush.toBuilderWithAndroidColor(testColor).build()
assertThat(newBrush.colorLong).isEqualTo(testColorLong)
assertThat(brush.family).isEqualTo(testFamily)
@@ -165,11 +154,11 @@
}
@Test
- fun brushUtilToBuilderWithAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
+ fun brushToBuilderWithAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
val brush = Brush.createWithColorIntArgb(testFamily, 0x4499bb66, 2f, 0.2f)
val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
- val newBrush = BrushUtil.toBuilderWithAndroidColor(brush, unsupportedColor).build()
+ val newBrush = brush.toBuilderWithAndroidColor(unsupportedColor).build()
// unsupportedColor gets converted to ColorLong (losing precision) and then to Display P3.
val expectedColor = AndroidColor.valueOf(unsupportedColor.pack()).convert(displayP3)
@@ -181,9 +170,9 @@
}
@Test
- fun brushUtilMakeBuilderWithAndroidColor_setsColor() {
+ fun createBrushBuilderWithAndroidColor_setsColor() {
val brush =
- BrushUtil.createBuilderWithAndroidColor(testColor)
+ createBrushBuilderWithAndroidColor(testColor)
.setFamily(testFamily)
.setSize(2f)
.setEpsilon(0.2f)
@@ -196,10 +185,10 @@
}
@Test
- fun brushUtilMakeBuilderAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
+ fun createBrushBuilderWithAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
val brush =
- BrushUtil.createBuilderWithAndroidColor(unsupportedColor)
+ createBrushBuilderWithAndroidColor(unsupportedColor)
.setFamily(testFamily)
.setSize(2f)
.setEpsilon(0.2f)
@@ -209,20 +198,4 @@
val expectedColor = AndroidColor.valueOf(unsupportedColor.pack()).convert(displayP3)
assertThat(brush.colorLong).isEqualTo(expectedColor.pack())
}
-
- @Test
- fun brushUtilMakeBrushWithAndroidColor_createsBrushWithColor() {
- val brush = BrushUtil.createWithAndroidColor(testFamily, testColor, 1f, 1f)
- assertThat(brush.colorLong).isEqualTo(testColorLong)
- }
-
- @Test
- fun brushUtilMakeBrushWithAndroidColor_withUnsupportedColorSpace_createsBrushWithConvertedColor() {
- val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
- val brush = BrushUtil.createWithAndroidColor(testFamily, unsupportedColor, 1f, 1f)
-
- // unsupportedColor gets converted to ColorLong (losing precision) and then to Display P3.
- val expectedColor = AndroidColor.valueOf(unsupportedColor.pack()).convert(displayP3)
- assertThat(brush.colorLong).isEqualTo(expectedColor.pack())
- }
}
diff --git a/ink/ink-brush/src/androidMain/kotlin/androidx/ink/brush/BrushExtensions.android.kt b/ink/ink-brush/src/androidMain/kotlin/androidx/ink/brush/BrushExtensions.android.kt
index a1a5bdc..ff89d66 100644
--- a/ink/ink-brush/src/androidMain/kotlin/androidx/ink/brush/BrushExtensions.android.kt
+++ b/ink/ink-brush/src/androidMain/kotlin/androidx/ink/brush/BrushExtensions.android.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+@file:JvmName("BrushUtil")
package androidx.ink.brush
@@ -22,17 +22,20 @@
import android.os.Build
import androidx.annotation.CheckResult
import androidx.annotation.RequiresApi
-import androidx.annotation.RestrictTo
/**
* The brush color as an [android.graphics.Color] instance, which can express colors in several
* different color spaces. sRGB and Display P3 are supported; a color in any other color space will
* be converted to Display P3.
+ *
+ * Unless an instance of [android.graphics.Color] is actually needed, prefer to use
+ * [Brush.colorLong] to get the color without causing an allocation, especially in
+ * performance-sensitive code. [Brush.colorLong] is fully compatible with the [Long] representation
+ * of [android.graphics.Color].
*/
-@JvmSynthetic
@CheckResult
@RequiresApi(Build.VERSION_CODES.O)
-public fun Brush.getAndroidColor(): AndroidColor = BrushUtil.getAndroidColor(this)
+public fun Brush.createAndroidColor(): AndroidColor = AndroidColor.valueOf(colorLong)
/**
* Creates a copy of `this` [Brush] and allows named properties to be altered while keeping the rest
@@ -40,7 +43,6 @@
* several different color spaces. sRGB and Display P3 are supported; a color in any other color
* space will be converted to Display P3.
*/
-@JvmSynthetic
@CheckResult
@RequiresApi(Build.VERSION_CODES.O)
public fun Brush.copyWithAndroidColor(
@@ -53,19 +55,53 @@
/**
* Set the color on a [Brush.Builder] as an [android.graphics.Color] instance. sRGB and Display P3
* are supported; a color in any other color space will be converted to Display P3.
+ *
+ * Java callers should prefer [toBuilderWithAndroidColor] or [createBrushBuilderWithAndroidColor] as
+ * a more fluent API.
*/
-@JvmSynthetic
@CheckResult
@RequiresApi(Build.VERSION_CODES.O)
public fun Brush.Builder.setAndroidColor(color: AndroidColor): Brush.Builder =
setColorLong(color.pack())
/**
+ * Returns a [Brush.Builder] with values set equivalent to the [Brush] and the color specified by an
+ * [android.graphics.Color] instance, which can encode several different color spaces. sRGB and
+ * Display P3 are supported; a color in any other color space will be converted to Display P3. Java
+ * developers, use the returned builder to build a copy of a Brush. Kotlin developers, see
+ * [copyWithAndroidColor] method.
+ *
+ * In Kotlin, calling this is equivalent to calling [Brush.toBuilder] followed by
+ * [Brush.Builder.setAndroidColor]. For Java callers, this function allows more fluent call
+ * chaining.
+ */
+@CheckResult
+@RequiresApi(Build.VERSION_CODES.O)
+public fun Brush.toBuilderWithAndroidColor(color: AndroidColor): Brush.Builder =
+ toBuilder().setAndroidColor(color)
+
+/**
+ * Returns a new, blank [Brush.Builder] with the color specified by an [android.graphics.Color]
+ * instance, which can encode several different color spaces. sRGB and Display P3 are supported; a
+ * color in any other color space will be converted to Display P3.
+ *
+ * In Kotlin, calling this is equivalent to calling [Brush.builder] followed by
+ * [Brush.Builder.setAndroidColor]. For Java callers, this function allows more fluent call
+ * chaining.
+ */
+@JvmName("createBuilderWithAndroidColor")
+@CheckResult
+@RequiresApi(Build.VERSION_CODES.O)
+public fun createBrushBuilderWithAndroidColor(color: AndroidColor): Brush.Builder =
+ Brush.Builder().setAndroidColor(color)
+
+/**
* Returns a new [Brush] with the color specified by an [android.graphics.Color] instance, which can
* encode several different color spaces. sRGB and Display P3 are supported; a color in any other
* color space will be converted to Display P3.
+ *
+ * Java callers should prefer `BrushUtil.createWithAndroidColor` ([createBrushWithAndroidColor]).
*/
-@JvmSynthetic
@CheckResult
@RequiresApi(Build.VERSION_CODES.O)
public fun Brush.Companion.createWithAndroidColor(
@@ -73,57 +109,21 @@
color: AndroidColor,
size: Float,
epsilon: Float,
-): Brush = BrushUtil.createWithAndroidColor(family, color, size, epsilon)
+): Brush = createWithColorLong(family, color.pack(), size, epsilon)
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
-public object BrushUtil {
-
- /**
- * The brush color as an [android.graphics.Color] instance, which can express colors in several
- * different color spaces. sRGB and Display P3 are supported; a color in any other color space
- * will be converted to Display P3.
- */
- @JvmStatic
- @CheckResult
- @RequiresApi(Build.VERSION_CODES.O)
- public fun getAndroidColor(brush: Brush): AndroidColor = AndroidColor.valueOf(brush.colorLong)
-
- /**
- * Returns a [Brush.Builder] with values set equivalent to [brush] and the color specified by an
- * [android.graphics.Color] instance, which can encode several different color spaces. sRGB and
- * Display P3 are supported; a color in any other color space will be converted to Display P3.
- * Java developers, use the returned builder to build a copy of a Brush. Kotlin developers, see
- * [copyWithAndroidColor] method.
- */
- @JvmStatic
- @CheckResult
- @RequiresApi(Build.VERSION_CODES.O)
- public fun toBuilderWithAndroidColor(brush: Brush, color: AndroidColor): Brush.Builder =
- brush.toBuilder().setAndroidColor(color)
-
- /**
- * Returns a new [Brush.Builder] with the color specified by an [android.graphics.Color]
- * instance, which can encode several different color spaces. sRGB and Display P3 are supported;
- * a color in any other color space will be converted to Display P3.
- */
- @JvmStatic
- @CheckResult
- @RequiresApi(Build.VERSION_CODES.O)
- public fun createBuilderWithAndroidColor(color: AndroidColor): Brush.Builder =
- Brush.Builder().setAndroidColor(color)
-
- /**
- * Returns a new [Brush] with the color specified by an [android.graphics.Color] instance, which
- * can encode several different color spaces. sRGB and Display P3 are supported; a color in any
- * other color space will be converted to Display P3.
- */
- @JvmStatic
- @CheckResult
- @RequiresApi(Build.VERSION_CODES.O)
- public fun createWithAndroidColor(
- family: BrushFamily,
- color: AndroidColor,
- size: Float,
- epsilon: Float,
- ): Brush = Brush.createWithColorLong(family, color.pack(), size, epsilon)
-}
+/**
+ * Returns a new [Brush] with the color specified by an [android.graphics.Color] instance, which can
+ * encode several different color spaces. sRGB and Display P3 are supported; a color in any other
+ * color space will be converted to Display P3.
+ *
+ * Kotlin callers should prefer [Brush.Companion.createWithAndroidColor].
+ */
+@JvmName("createWithAndroidColor")
+@CheckResult
+@RequiresApi(Build.VERSION_CODES.O)
+public fun createBrushWithAndroidColor(
+ family: BrushFamily,
+ color: AndroidColor,
+ size: Float,
+ epsilon: Float,
+): Brush = Brush.createWithAndroidColor(family, color, size, epsilon)
diff --git a/ink/ink-geometry/api/current.txt b/ink/ink-geometry/api/current.txt
index 7d352f46..8901f35 100644
--- a/ink/ink-geometry/api/current.txt
+++ b/ink/ink-geometry/api/current.txt
@@ -65,6 +65,7 @@
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Box? box);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.BoxAccumulator? other);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Parallelogram parallelogram);
+ method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.PartitionedMesh mesh);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Segment segment);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Triangle triangle);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Vec point);
@@ -172,26 +173,37 @@
public final class Intersection {
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Box other);
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToBox);
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Segment segment);
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Triangle triangle);
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Vec point);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Box box);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Parallelogram other);
+ method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToParallelogram);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Segment segment);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Triangle triangle);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Vec point);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Box box, androidx.ink.geometry.AffineTransform meshToBox);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Parallelogram parallelogram, androidx.ink.geometry.AffineTransform meshToParallelogram);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.PartitionedMesh other, androidx.ink.geometry.AffineTransform thisToCommonTransForm, androidx.ink.geometry.AffineTransform otherToCommonTransform);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Segment segment, androidx.ink.geometry.AffineTransform meshToSegment);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Triangle triangle, androidx.ink.geometry.AffineTransform meshToTriangle);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Vec point, androidx.ink.geometry.AffineTransform meshToPoint);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Box box);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToSegment);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Segment other);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Triangle triangle);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Vec point);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Box box);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToTriangle);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Segment segment);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Triangle other);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Vec point);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Box box);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToPoint);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Segment segment);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Triangle triangle);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Vec other);
@@ -313,6 +325,36 @@
public static final class Parallelogram.Companion {
}
+ public final class PartitionedMesh {
+ method public androidx.ink.geometry.Box? computeBoundingBox();
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Box box);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Box box, optional androidx.ink.geometry.AffineTransform boxToThis);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Parallelogram parallelogram);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Parallelogram parallelogram, optional androidx.ink.geometry.AffineTransform parallelogramToThis);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.PartitionedMesh other);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.PartitionedMesh other, optional androidx.ink.geometry.AffineTransform otherShapeToThis);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Triangle triangle);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Triangle triangle, optional androidx.ink.geometry.AffineTransform triangleToThis);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Box box, float coverageThreshold);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Box box, float coverageThreshold, optional androidx.ink.geometry.AffineTransform boxToThis);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Parallelogram parallelogram, float coverageThreshold);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Parallelogram parallelogram, float coverageThreshold, optional androidx.ink.geometry.AffineTransform parallelogramToThis);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.PartitionedMesh other, float coverageThreshold);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.PartitionedMesh other, float coverageThreshold, optional androidx.ink.geometry.AffineTransform otherShapeToThis);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Triangle triangle, float coverageThreshold);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Triangle triangle, float coverageThreshold, optional androidx.ink.geometry.AffineTransform triangleToThis);
+ method protected void finalize();
+ method @IntRange(from=0L) public int getOutlineCount(@IntRange(from=0L) int groupIndex);
+ method @IntRange(from=0L) public int getOutlineVertexCount(@IntRange(from=0L) int groupIndex, @IntRange(from=0L) int outlineIndex);
+ method @IntRange(from=0L) public int getRenderGroupCount();
+ method public void initializeSpatialIndex();
+ method public androidx.ink.geometry.MutableVec populateOutlinePosition(@IntRange(from=0L) int groupIndex, @IntRange(from=0L) int outlineIndex, @IntRange(from=0L) int outlineVertexIndex, androidx.ink.geometry.MutableVec outPosition);
+ field public static final androidx.ink.geometry.PartitionedMesh.Companion Companion;
+ }
+
+ public static final class PartitionedMesh.Companion {
+ }
+
public abstract class Segment {
method public final androidx.ink.geometry.ImmutableBox computeBoundingBox();
method public final androidx.ink.geometry.MutableBox computeBoundingBox(androidx.ink.geometry.MutableBox outBox);
diff --git a/ink/ink-geometry/api/restricted_current.txt b/ink/ink-geometry/api/restricted_current.txt
index 7d352f46..8901f35 100644
--- a/ink/ink-geometry/api/restricted_current.txt
+++ b/ink/ink-geometry/api/restricted_current.txt
@@ -65,6 +65,7 @@
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Box? box);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.BoxAccumulator? other);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Parallelogram parallelogram);
+ method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.PartitionedMesh mesh);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Segment segment);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Triangle triangle);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Vec point);
@@ -172,26 +173,37 @@
public final class Intersection {
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Box other);
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToBox);
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Segment segment);
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Triangle triangle);
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Vec point);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Box box);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Parallelogram other);
+ method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToParallelogram);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Segment segment);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Triangle triangle);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Vec point);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Box box, androidx.ink.geometry.AffineTransform meshToBox);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Parallelogram parallelogram, androidx.ink.geometry.AffineTransform meshToParallelogram);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.PartitionedMesh other, androidx.ink.geometry.AffineTransform thisToCommonTransForm, androidx.ink.geometry.AffineTransform otherToCommonTransform);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Segment segment, androidx.ink.geometry.AffineTransform meshToSegment);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Triangle triangle, androidx.ink.geometry.AffineTransform meshToTriangle);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Vec point, androidx.ink.geometry.AffineTransform meshToPoint);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Box box);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToSegment);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Segment other);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Triangle triangle);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Vec point);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Box box);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToTriangle);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Segment segment);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Triangle other);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Vec point);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Box box);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToPoint);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Segment segment);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Triangle triangle);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Vec other);
@@ -313,6 +325,36 @@
public static final class Parallelogram.Companion {
}
+ public final class PartitionedMesh {
+ method public androidx.ink.geometry.Box? computeBoundingBox();
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Box box);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Box box, optional androidx.ink.geometry.AffineTransform boxToThis);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Parallelogram parallelogram);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Parallelogram parallelogram, optional androidx.ink.geometry.AffineTransform parallelogramToThis);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.PartitionedMesh other);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.PartitionedMesh other, optional androidx.ink.geometry.AffineTransform otherShapeToThis);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Triangle triangle);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Triangle triangle, optional androidx.ink.geometry.AffineTransform triangleToThis);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Box box, float coverageThreshold);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Box box, float coverageThreshold, optional androidx.ink.geometry.AffineTransform boxToThis);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Parallelogram parallelogram, float coverageThreshold);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Parallelogram parallelogram, float coverageThreshold, optional androidx.ink.geometry.AffineTransform parallelogramToThis);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.PartitionedMesh other, float coverageThreshold);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.PartitionedMesh other, float coverageThreshold, optional androidx.ink.geometry.AffineTransform otherShapeToThis);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Triangle triangle, float coverageThreshold);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Triangle triangle, float coverageThreshold, optional androidx.ink.geometry.AffineTransform triangleToThis);
+ method protected void finalize();
+ method @IntRange(from=0L) public int getOutlineCount(@IntRange(from=0L) int groupIndex);
+ method @IntRange(from=0L) public int getOutlineVertexCount(@IntRange(from=0L) int groupIndex, @IntRange(from=0L) int outlineIndex);
+ method @IntRange(from=0L) public int getRenderGroupCount();
+ method public void initializeSpatialIndex();
+ method public androidx.ink.geometry.MutableVec populateOutlinePosition(@IntRange(from=0L) int groupIndex, @IntRange(from=0L) int outlineIndex, @IntRange(from=0L) int outlineVertexIndex, androidx.ink.geometry.MutableVec outPosition);
+ field public static final androidx.ink.geometry.PartitionedMesh.Companion Companion;
+ }
+
+ public static final class PartitionedMesh.Companion {
+ }
+
public abstract class Segment {
method public final androidx.ink.geometry.ImmutableBox computeBoundingBox();
method public final androidx.ink.geometry.MutableBox computeBoundingBox(androidx.ink.geometry.MutableBox outBox);
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/BoxAccumulator.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/BoxAccumulator.kt
index 79501d8..dde4d23 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/BoxAccumulator.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/BoxAccumulator.kt
@@ -227,8 +227,7 @@
*
* @return `this`
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
- public fun add(mesh: PartitionedMesh): BoxAccumulator = this.add(mesh.bounds)
+ public fun add(mesh: PartitionedMesh): BoxAccumulator = this.add(mesh.computeBoundingBox())
/**
* Compares this [BoxAccumulator] with [other], and returns true if either: Both this and
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Intersection.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Intersection.kt
index 1ee275d..fcdf2f5 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Intersection.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Intersection.kt
@@ -16,7 +16,6 @@
package androidx.ink.geometry
-import androidx.annotation.RestrictTo
import androidx.ink.nativeloader.NativeLoader
/**
@@ -116,7 +115,6 @@
* intersection of the point in [mesh]’s object coordinates.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun Vec.intersects(mesh: PartitionedMesh, meshToPoint: AffineTransform): Boolean {
return nativeMeshVecIntersects(
nativeMeshAddress = mesh.getNativeAddress(),
@@ -218,7 +216,6 @@
* coordinate space to the coordinate space that the intersection should be checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun Segment.intersects(mesh: PartitionedMesh, meshToSegment: AffineTransform): Boolean {
return nativeMeshSegmentIntersects(
nativeMeshAddress = mesh.getNativeAddress(),
@@ -311,7 +308,6 @@
* coordinate space to the coordinate space that the intersection should be checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun Triangle.intersects(
mesh: PartitionedMesh,
meshToTriangle: AffineTransform
@@ -382,7 +378,6 @@
* coordinate space to the coordinate space that the intersection should be checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun Box.intersects(mesh: PartitionedMesh, meshToBox: AffineTransform): Boolean {
return nativeMeshBoxIntersects(
nativeMeshAddress = mesh.getNativeAddress(),
@@ -436,7 +431,6 @@
* checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun Parallelogram.intersects(
mesh: PartitionedMesh,
meshToParallelogram: AffineTransform,
@@ -467,7 +461,6 @@
* coordinate space that the intersection should be checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun PartitionedMesh.intersects(
other: PartitionedMesh,
thisToCommonTransForm: AffineTransform,
@@ -566,7 +559,6 @@
* intersection of the point in [mesh]’s object coordinates.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun PartitionedMesh.intersects(point: Vec, meshToPoint: AffineTransform): Boolean =
point.intersects(this, meshToPoint)
@@ -578,7 +570,6 @@
* coordinate space to the coordinate space that the intersection should be checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun PartitionedMesh.intersects(
segment: Segment,
meshToSegment: AffineTransform
@@ -592,7 +583,6 @@
* coordinate space to the coordinate space that the intersection should be checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun PartitionedMesh.intersects(
triangle: Triangle,
meshToTriangle: AffineTransform,
@@ -606,7 +596,6 @@
* coordinate space to the coordinate space that the intersection should be checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun PartitionedMesh.intersects(box: Box, meshToBox: AffineTransform): Boolean =
box.intersects(this, meshToBox)
@@ -619,7 +608,6 @@
* checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun PartitionedMesh.intersects(
parallelogram: Parallelogram,
meshToParallelogram: AffineTransform,
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/PartitionedMesh.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/PartitionedMesh.kt
index d279937..6f186f6 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/PartitionedMesh.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/PartitionedMesh.kt
@@ -25,23 +25,22 @@
import androidx.ink.nativeloader.NativeLoader
/**
- * An immutable† complex shape expressed as a set of triangles. This is used to represent the shape
- * of a stroke or other complex objects see [MeshCreation]. The mesh may be divided into multiple
- * partitions, which enables certain brush effects (e.g. "multi-coat"), and allows ink to create
- * strokes requiring greater than 216 triangles (which must be rendered in multiple passes).
+ * An immutable** complex shape expressed as a set of triangles. This is used to represent the shape
+ * of a stroke or other complex objects. The mesh may be divided into multiple partitions, which
+ * enables certain brush effects (e.g. "multi-coat"), and allows strokes to be created using greater
+ * than 2^16 triangles (which must be rendered in multiple passes).
*
- * A PartitionedMesh may optionally have one or more "outlines", which are polylines that traverse
+ * A [PartitionedMesh] may optionally have one or more "outlines", which are polylines that traverse
* some or all of the vertices in the mesh; these are used for path-based rendering of strokes. This
* supports disjoint meshes such as dashed lines.
*
- * PartitionedMesh provides fast intersection and coverage testing by use of an internal spatial
+ * [PartitionedMesh] provides fast intersection and coverage testing by use of an internal spatial
* index.
*
- * † PartitionedMesh is technically not immutable, as the spatial index is lazily instantiated;
+ * ** [PartitionedMesh] is technically not immutable, as the spatial index is lazily instantiated;
* however, from the perspective of a caller, its properties do not change over the course of its
* lifetime. The entire object is thread-safe.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
@Suppress("NotCloseable") // Finalize is only used to free the native peer.
public class PartitionedMesh
/** Only for use within the ink library. Constructs a [PartitionedMesh] from native pointer. */
@@ -73,42 +72,53 @@
@VisibleForTesting internal constructor() : this(ModeledShapeNative.alloc())
/**
- * The number of render groups in this mesh. Each outline in the [PartitionedMesh] belongs to
- * exactly one render group, which are numbered in z-order: the group with index zero should be
- * rendered on bottom; the group with the highest index should be rendered on top.
+ * Returns the number of render groups in this mesh. Each outline in the [PartitionedMesh]
+ * belongs to exactly one render group, which are numbered in z-order: the group with index zero
+ * should be rendered on bottom; the group with the highest index should be rendered on top.
*/
@IntRange(from = 0)
- public val renderGroupCount: Int =
+ public fun getRenderGroupCount(): Int =
ModeledShapeNative.getRenderGroupCount(nativeAddress).also { check(it >= 0) }
/** The [Mesh] objects that make up this shape. */
private val meshesByGroup: List<List<Mesh>> = buildList {
- for (groupIndex in 0 until renderGroupCount) {
+ for (groupIndex in 0 until getRenderGroupCount()) {
val nativeAddressesOfMeshes =
ModeledShapeNative.getNativeAddressesOfMeshes(nativeAddress, groupIndex)
add(nativeAddressesOfMeshes.map(::Mesh))
}
}
+ private var _bounds: Box? = null
+
/**
- * The minimum bounding box of the [PartitionedMesh]. This will be null if the [PartitionedMesh]
- * is empty.
+ * Returns the minimum bounding box of the [PartitionedMesh]. This will be null if the
+ * [PartitionedMesh] is empty.
*/
- public val bounds: Box? = run {
+ public fun computeBoundingBox(): Box? {
+ // If we've already computed the bounding box, re-use it -- it won't change over the
+ // lifetime of
+ // this object.
+ if (_bounds != null) return _bounds
+
+ // If we have no meshes, then the bounding box is null.
+ if (meshesByGroup.isEmpty()) return null
+
val envelope = BoxAccumulator()
for (meshes in meshesByGroup) {
for (mesh in meshes) {
envelope.add(mesh.bounds)
}
}
- envelope.box
+ _bounds = envelope.box
+ return envelope.box
}
/** Returns the [MeshFormat] used for each [Mesh] in the specified render group. */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
public fun renderGroupFormat(@IntRange(from = 0) groupIndex: Int): MeshFormat {
- require(groupIndex >= 0 && groupIndex < renderGroupCount) {
- "groupIndex=$groupIndex must be between 0 and renderGroupCount=${renderGroupCount}"
+ require(groupIndex >= 0 && groupIndex < getRenderGroupCount()) {
+ "groupIndex=$groupIndex must be between 0 and renderGroupCount=${getRenderGroupCount()}"
}
return MeshFormat(ModeledShapeNative.getRenderGroupFormat(nativeAddress, groupIndex))
}
@@ -119,51 +129,51 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
public fun renderGroupMeshes(@IntRange(from = 0) groupIndex: Int): List<Mesh> {
- require(groupIndex >= 0 && groupIndex < renderGroupCount) {
- "groupIndex=$groupIndex must be between 0 and renderGroupCount=${renderGroupCount}"
+ require(groupIndex >= 0 && groupIndex < getRenderGroupCount()) {
+ "groupIndex=$groupIndex must be between 0 and renderGroupCount=${getRenderGroupCount()}"
}
return meshesByGroup[groupIndex]
}
- /** The number of outlines that comprise this shape. */
+ /** Returns the number of outlines that comprise this shape. */
@IntRange(from = 0)
- public fun outlineCount(@IntRange(from = 0) groupIndex: Int): Int {
- require(groupIndex >= 0 && groupIndex < renderGroupCount) {
- "groupIndex=$groupIndex must be between 0 and renderGroupCount=${renderGroupCount}"
+ public fun getOutlineCount(@IntRange(from = 0) groupIndex: Int): Int {
+ require(groupIndex >= 0 && groupIndex < getRenderGroupCount()) {
+ "groupIndex=$groupIndex must be between 0 and renderGroupCount=${getRenderGroupCount()}"
}
return ModeledShapeNative.getOutlineCount(nativeAddress, groupIndex).also { check(it >= 0) }
}
/**
- * The number of vertices that are in the outline at index [outlineIndex], and within the render
- * group at [groupIndex].
+ * Returns the number of vertices that are in the outline at index [outlineIndex], and within
+ * the render group at [groupIndex].
*/
@IntRange(from = 0)
- public fun outlineVertexCount(
+ public fun getOutlineVertexCount(
@IntRange(from = 0) groupIndex: Int,
@IntRange(from = 0) outlineIndex: Int,
): Int {
- require(outlineIndex >= 0 && outlineIndex < outlineCount(groupIndex)) {
- "outlineIndex=$outlineIndex must be between 0 and outlineCount=${outlineCount(groupIndex)}"
+ require(outlineIndex >= 0 && outlineIndex < getOutlineCount(groupIndex)) {
+ "outlineIndex=$outlineIndex must be between 0 and outlineCount=${getOutlineCount(groupIndex)}"
}
return ModeledShapeNative.getOutlineVertexCount(nativeAddress, groupIndex, outlineIndex)
.also { check(it >= 0) }
}
/**
- * Retrieve the outline vertex position from the outline at index [outlineIndex] (which can be
- * up to, but not including, [outlineCount]), and the vertex from within that outline at index
- * [outlineVertexIndex] (which can be up to, but not including, the result of calling
- * [outlineVertexCount] with [outlineIndex]). The resulting x/y position of that outline vertex
- * will be put into [outPosition], which can be pre-allocated and reused to avoid allocations.
+ * Populates [outPosition] with the position of the outline vertex at [outlineVertexIndex] in
+ * the outline at [outlineIndex] in the render group at [groupIndex], and returns [outPosition].
+ * [groupIndex] must be less than [getRenderGroupCount], [outlineIndex] must be less
+ * [getOutlineVertexCount] for [groupIndex], and [outlineVertexIndex] must be less than
+ * [getOutlineVertexCount] for [groupIndex] and [outlineIndex].
*/
public fun populateOutlinePosition(
@IntRange(from = 0) groupIndex: Int,
@IntRange(from = 0) outlineIndex: Int,
@IntRange(from = 0) outlineVertexIndex: Int,
outPosition: MutableVec,
- ) {
- val outlineVertexCount = outlineVertexCount(groupIndex, outlineIndex)
+ ): MutableVec {
+ val outlineVertexCount = getOutlineVertexCount(groupIndex, outlineIndex)
require(outlineVertexIndex >= 0 && outlineVertexIndex < outlineVertexCount) {
"outlineVertexIndex=$outlineVertexIndex must be between 0 and " +
"outlineVertexCount($outlineVertexIndex)=$outlineVertexCount"
@@ -178,6 +188,7 @@
val (meshIndex, meshVertexIndex) = scratchIntArray
val mesh = meshesByGroup[groupIndex][meshIndex]
mesh.fillPosition(meshVertexIndex, outPosition)
+ return outPosition
}
/**
@@ -187,18 +198,18 @@
* triangles in the [PartitionedMesh], all in the [PartitionedMesh]'s coordinate space.
* Triangles in the [PartitionedMesh] that overlap each other (e.g. in the case of a stroke that
* loops back over itself) are counted individually. Note that, if any triangles have negative
- * area (due to winding, see [com.google.inputmethod.ink.Triangle.signedArea]), the absolute
- * value of their area will be used instead.
+ * area (due to winding, see [Triangle.computeSignedArea]), the absolute value of their area
+ * will be used instead.
*
* On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [triangleToThis] contains the transform that maps from [triangle]'s
- * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to the
- * [IDENTITY].
+ * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to
+ * [AffineTransform.IDENTITY].
*/
@JvmOverloads
@FloatRange(from = 0.0, to = 1.0)
- public fun coverage(
+ public fun computeCoverage(
triangle: Triangle,
triangleToThis: AffineTransform = AffineTransform.IDENTITY,
): Float =
@@ -225,17 +236,20 @@
* [PartitionedMesh], all in the [PartitionedMesh]'s coordinate space. Triangles in the
* [PartitionedMesh] that overlap each other (e.g. in the case of a stroke that loops back over
* itself) are counted individually. Note that, if any triangles have negative area (due to
- * winding, see [com.google.inputmethod.ink.Triangle.signedArea]), the absolute value of their
- * area will be used instead.
+ * winding, see [Triangle.computeSignedArea]), the absolute value of their area will be used
+ * instead.
*
* On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [boxToThis] contains the transform that maps from [box]'s coordinate space
- * to this [PartitionedMesh]'s coordinate space, which defaults to the [IDENTITY].
+ * to this [PartitionedMesh]'s coordinate space, which defaults to [AffineTransform.IDENTITY].
*/
@JvmOverloads
@FloatRange(from = 0.0, to = 1.0)
- public fun coverage(box: Box, boxToThis: AffineTransform = AffineTransform.IDENTITY): Float =
+ public fun computeCoverage(
+ box: Box,
+ boxToThis: AffineTransform = AffineTransform.IDENTITY
+ ): Float =
ModeledShapeNative.modeledShapeBoxCoverage(
nativeAddress = nativeAddress,
boxXMin = box.xMin,
@@ -257,18 +271,18 @@
* of all triangles in the [PartitionedMesh], all in the [PartitionedMesh]'s coordinate space.
* Triangles in the [PartitionedMesh] that overlap each other (e.g. in the case of a stroke that
* loops back over itself) are counted individually. Note that, if any triangles have negative
- * area (due to winding, see [com.google.inputmethod.ink.Triangle.signedArea]), the absolute
- * value of their area will be used instead.
+ * area (due to winding, see [Triangle.computeSignedArea]), the absolute value of their area
+ * will be used instead.
*
* On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [parallelogramToThis] contains the transform that maps from
* [parallelogram]'s coordinate space to this [PartitionedMesh]'s coordinate space, which
- * defaults to the [IDENTITY].
+ * defaults to [AffineTransform.IDENTITY].
*/
@JvmOverloads
@FloatRange(from = 0.0, to = 1.0)
- public fun coverage(
+ public fun computeCoverage(
parallelogram: Parallelogram,
parallelogramToThis: AffineTransform = AffineTransform.IDENTITY,
): Float =
@@ -295,18 +309,18 @@
* triangles in the [PartitionedMesh], all in the [PartitionedMesh]'s coordinate space.
* Triangles in the [PartitionedMesh] that overlap each other (e.g. in the case of a stroke that
* loops back over itself) are counted individually. Note that, if any triangles have negative
- * area (due to winding, see [com.google.inputmethod.ink.Triangle.signedArea]), the absolute
- * value of their area will be used instead.t
+ * area (due to winding, see [Triangle.computeSignedArea]), the absolute value of their area
+ * will be used instead.
*
* On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [otherShapeToThis] contains the transform that maps from [other]'s
- * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to the
- * [IDENTITY].
+ * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to
+ * [AffineTransform.IDENTITY].
*/
@JvmOverloads
@FloatRange(from = 0.0, to = 1.0)
- public fun coverage(
+ public fun computeCoverage(
other: PartitionedMesh,
otherShapeToThis: AffineTransform = AffineTransform.IDENTITY,
): Float =
@@ -327,7 +341,7 @@
*
* This is equivalent to:
* ```
- * this.coverage(triangle, triangleToThis) > coverageThreshold
+ * computeCoverage(triangle, triangleToThis) > coverageThreshold
* ```
*
* but may be faster.
@@ -335,11 +349,11 @@
* On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [triangleToThis] contains the transform that maps from [triangle]'s
- * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to the
- * [IDENTITY].
+ * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to
+ * [AffineTransform.IDENTITY].
*/
@JvmOverloads
- public fun coverageIsGreaterThan(
+ public fun computeCoverageIsGreaterThan(
triangle: Triangle,
coverageThreshold: Float,
triangleToThis: AffineTransform = AffineTransform.IDENTITY,
@@ -375,10 +389,10 @@
* On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [boxToThis] contains the transform that maps from [box]'s coordinate space
- * to this [PartitionedMesh]'s coordinate space, which defaults to the [IDENTITY].
+ * to this [PartitionedMesh]'s coordinate space, which defaults to [AffineTransform.IDENTITY].
*/
@JvmOverloads
- public fun coverageIsGreaterThan(
+ public fun computeCoverageIsGreaterThan(
box: Box,
coverageThreshold: Float,
boxToThis: AffineTransform = AffineTransform.IDENTITY,
@@ -413,10 +427,10 @@
*
* Optional argument [parallelogramToThis] contains the transform that maps from
* [parallelogram]'s coordinate space to this [PartitionedMesh]'s coordinate space, which
- * defaults to the [IDENTITY].
+ * defaults to [AffineTransform.IDENTITY].
*/
@JvmOverloads
- public fun coverageIsGreaterThan(
+ public fun computeCoverageIsGreaterThan(
parallelogram: Parallelogram,
coverageThreshold: Float,
parallelogramToThis: AffineTransform = AffineTransform.IDENTITY,
@@ -452,11 +466,11 @@
* On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [otherShapeToThis] contains the transform that maps from [other]'s
- * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to the
- * [IDENTITY].
+ * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to
+ * [AffineTransform.IDENTITY].
*/
@JvmOverloads
- public fun coverageIsGreaterThan(
+ public fun computeCoverageIsGreaterThan(
other: PartitionedMesh,
coverageThreshold: Float,
otherShapeToThis: AffineTransform = AffineTransform.IDENTITY,
@@ -488,7 +502,7 @@
override fun toString(): String {
val address = java.lang.Long.toHexString(nativeAddress)
- return "PartitionedMesh(bounds=$bounds, meshesByGroup=$meshesByGroup, nativeAddress=$address)"
+ return "PartitionedMesh(bounds=${computeBoundingBox()}, meshesByGroup=$meshesByGroup, nativeAddress=$address)"
}
protected fun finalize() {
diff --git a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/PartitionedMeshTest.kt b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/PartitionedMeshTest.kt
index 10bf22b..50106fe 100644
--- a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/PartitionedMeshTest.kt
+++ b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/PartitionedMeshTest.kt
@@ -31,35 +31,35 @@
class PartitionedMeshTest {
@Test
- fun bounds_shouldBeEmpty() {
+ fun computeBoundingBox_shouldBeEmpty() {
val partitionedMesh = PartitionedMesh()
- assertThat(partitionedMesh.bounds).isNull()
+ assertThat(partitionedMesh.computeBoundingBox()).isNull()
}
@Test
- fun renderGroupCount_whenEmptyShape_shouldBeZero() {
+ fun getRenderGroupCount_whenEmptyShape_shouldBeZero() {
val partitionedMesh = PartitionedMesh()
- assertThat(partitionedMesh.renderGroupCount).isEqualTo(0)
+ assertThat(partitionedMesh.getRenderGroupCount()).isEqualTo(0)
}
@Test
- fun outlineCount_whenEmptyShape_shouldThrow() {
+ fun getOutlineCount_whenEmptyShape_shouldThrow() {
val partitionedMesh = PartitionedMesh()
- assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineCount(-1) }
- assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineCount(0) }
- assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineCount(1) }
+ assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineCount(-1) }
+ assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineCount(0) }
+ assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineCount(1) }
}
@Test
- fun outlineVertexCount_whenEmptyShape_shouldThrow() {
+ fun getOutlineVertexCount_whenEmptyShape_shouldThrow() {
val partitionedMesh = PartitionedMesh()
- assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineVertexCount(-1, 0) }
- assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineVertexCount(0, 0) }
- assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineVertexCount(1, 0) }
+ assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineVertexCount(-1, 0) }
+ assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineVertexCount(0, 0) }
+ assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineVertexCount(1, 0) }
}
@Test
@@ -92,14 +92,14 @@
fun populateOutlinePosition_withStrokeShape_shouldBeWithinBounds() {
val shape = buildTestStrokeShape()
- assertThat(shape.renderGroupCount).isEqualTo(1)
- assertThat(shape.outlineCount(0)).isEqualTo(1)
- assertThat(shape.outlineVertexCount(0, 0)).isGreaterThan(2)
+ assertThat(shape.getRenderGroupCount()).isEqualTo(1)
+ assertThat(shape.getOutlineCount(0)).isEqualTo(1)
+ assertThat(shape.getOutlineVertexCount(0, 0)).isGreaterThan(2)
- val bounds = assertNotNull(shape.bounds)
+ val bounds = assertNotNull(shape.computeBoundingBox())
val p = MutableVec()
- for (outlineVertexIndex in 0 until shape.outlineVertexCount(0, 0)) {
+ for (outlineVertexIndex in 0 until shape.getOutlineVertexCount(0, 0)) {
shape.populateOutlinePosition(groupIndex = 0, outlineIndex = 0, outlineVertexIndex, p)
assertThat(p.x).isAtLeast(bounds.xMin)
assertThat(p.y).isAtLeast(bounds.yMin)
@@ -124,7 +124,7 @@
@Test
fun meshFormat_forTestShape_isEquivalentToMeshFormatOfFirstMesh() {
val partitionedMesh = buildTestStrokeShape()
- assertThat(partitionedMesh.renderGroupCount).isEqualTo(1)
+ assertThat(partitionedMesh.getRenderGroupCount()).isEqualTo(1)
val shapeFormat = partitionedMesh.renderGroupFormat(0)
val meshes = partitionedMesh.renderGroupMeshes(0)
assertThat(meshes).isNotEmpty()
@@ -152,9 +152,9 @@
p2 = ImmutableVec(100f, 700f),
)
- assertThat(partitionedMesh.coverage(intersectingTriangle)).isGreaterThan(0f)
- assertThat(partitionedMesh.coverage(externalTriangle)).isEqualTo(0f)
- assertThat(partitionedMesh.coverage(externalTriangle, SCALE_TRANSFORM)).isEqualTo(0f)
+ assertThat(partitionedMesh.computeCoverage(intersectingTriangle)).isGreaterThan(0f)
+ assertThat(partitionedMesh.computeCoverage(externalTriangle)).isEqualTo(0f)
+ assertThat(partitionedMesh.computeCoverage(externalTriangle, SCALE_TRANSFORM)).isEqualTo(0f)
}
/**
@@ -169,9 +169,9 @@
val externalBox =
ImmutableBox.fromTwoPoints(ImmutableVec(100f, 200f), ImmutableVec(300f, 400f))
- assertThat(partitionedMesh.coverage(intersectingBox)).isGreaterThan(0f)
- assertThat(partitionedMesh.coverage(externalBox)).isEqualTo(0f)
- assertThat(partitionedMesh.coverage(externalBox, SCALE_TRANSFORM)).isEqualTo(0f)
+ assertThat(partitionedMesh.computeCoverage(intersectingBox)).isGreaterThan(0f)
+ assertThat(partitionedMesh.computeCoverage(externalBox)).isEqualTo(0f)
+ assertThat(partitionedMesh.computeCoverage(externalBox, SCALE_TRANSFORM)).isEqualTo(0f)
}
/**
@@ -196,9 +196,10 @@
shearFactor = 2f,
)
- assertThat(partitionedMesh.coverage(intersectingParallelogram)).isGreaterThan(0f)
- assertThat(partitionedMesh.coverage(externalParallelogram)).isEqualTo(0f)
- assertThat(partitionedMesh.coverage(externalParallelogram, SCALE_TRANSFORM)).isEqualTo(0f)
+ assertThat(partitionedMesh.computeCoverage(intersectingParallelogram)).isGreaterThan(0f)
+ assertThat(partitionedMesh.computeCoverage(externalParallelogram)).isEqualTo(0f)
+ assertThat(partitionedMesh.computeCoverage(externalParallelogram, SCALE_TRANSFORM))
+ .isEqualTo(0f)
}
/**
@@ -221,9 +222,9 @@
)
.shape
- assertThat(partitionedMesh.coverage(intersectingShape)).isGreaterThan(0f)
- assertThat(partitionedMesh.coverage(externalShape)).isEqualTo(0f)
- assertThat(partitionedMesh.coverage(externalShape, SCALE_TRANSFORM)).isEqualTo(0f)
+ assertThat(partitionedMesh.computeCoverage(intersectingShape)).isGreaterThan(0f)
+ assertThat(partitionedMesh.computeCoverage(externalShape)).isEqualTo(0f)
+ assertThat(partitionedMesh.computeCoverage(externalShape, SCALE_TRANSFORM)).isEqualTo(0f)
}
/**
@@ -246,9 +247,11 @@
p2 = ImmutableVec(100f, 700f),
)
- assertThat(partitionedMesh.coverageIsGreaterThan(intersectingTriangle, 0f)).isTrue()
- assertThat(partitionedMesh.coverageIsGreaterThan(externalTriangle, 0f)).isFalse()
- assertThat(partitionedMesh.coverageIsGreaterThan(externalTriangle, 0f, SCALE_TRANSFORM))
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(intersectingTriangle, 0f)).isTrue()
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalTriangle, 0f)).isFalse()
+ assertThat(
+ partitionedMesh.computeCoverageIsGreaterThan(externalTriangle, 0f, SCALE_TRANSFORM)
+ )
.isFalse()
}
@@ -270,9 +273,9 @@
val externalBox =
ImmutableBox.fromTwoPoints(ImmutableVec(100f, 200f), ImmutableVec(300f, 400f))
- assertThat(partitionedMesh.coverageIsGreaterThan(intersectingBox, 0f)).isTrue()
- assertThat(partitionedMesh.coverageIsGreaterThan(externalBox, 0f)).isFalse()
- assertThat(partitionedMesh.coverageIsGreaterThan(externalBox, 0f, SCALE_TRANSFORM))
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(intersectingBox, 0f)).isTrue()
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalBox, 0f)).isFalse()
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalBox, 0f, SCALE_TRANSFORM))
.isFalse()
}
@@ -298,10 +301,16 @@
shearFactor = 2f,
)
- assertThat(partitionedMesh.coverageIsGreaterThan(intersectingParallelogram, 0f)).isTrue()
- assertThat(partitionedMesh.coverageIsGreaterThan(externalParallelogram, 0f)).isFalse()
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(intersectingParallelogram, 0f))
+ .isTrue()
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalParallelogram, 0f))
+ .isFalse()
assertThat(
- partitionedMesh.coverageIsGreaterThan(externalParallelogram, 0f, SCALE_TRANSFORM)
+ partitionedMesh.computeCoverageIsGreaterThan(
+ externalParallelogram,
+ 0f,
+ SCALE_TRANSFORM
+ )
)
.isFalse()
}
@@ -333,9 +342,9 @@
)
.shape
- assertThat(partitionedMesh.coverageIsGreaterThan(intersectingShape, 0f)).isTrue()
- assertThat(partitionedMesh.coverageIsGreaterThan(externalShape, 0f)).isFalse()
- assertThat(partitionedMesh.coverageIsGreaterThan(externalShape, 0f, SCALE_TRANSFORM))
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(intersectingShape, 0f)).isTrue()
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalShape, 0f)).isFalse()
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalShape, 0f, SCALE_TRANSFORM))
.isFalse()
}
@@ -360,7 +369,7 @@
)
assertThat(partitionedMesh.isSpatialIndexInitialized()).isFalse()
- assertThat(partitionedMesh.coverage(triangle)).isNotNaN()
+ assertThat(partitionedMesh.computeCoverage(triangle)).isNotNaN()
assertThat(partitionedMesh.isSpatialIndexInitialized()).isTrue()
}
diff --git a/ink/ink-strokes/api/current.txt b/ink/ink-strokes/api/current.txt
index e6f50d0..a81e872 100644
--- a/ink/ink-strokes/api/current.txt
+++ b/ink/ink-strokes/api/current.txt
@@ -1 +1,157 @@
// Signature format: 4.0
+package androidx.ink.strokes {
+
+ public final class ImmutableStrokeInputBatch extends androidx.ink.strokes.StrokeInputBatch {
+ field public static final androidx.ink.strokes.ImmutableStrokeInputBatch.Companion Companion;
+ field public static final androidx.ink.strokes.ImmutableStrokeInputBatch EMPTY;
+ }
+
+ public static final class ImmutableStrokeInputBatch.Companion {
+ }
+
+ public final class InProgressStroke {
+ ctor public InProgressStroke();
+ method public Object enqueueInputs(androidx.ink.strokes.StrokeInputBatch realInputs, androidx.ink.strokes.StrokeInputBatch predictedInputs);
+ method public void enqueueInputsOrThrow(androidx.ink.strokes.StrokeInputBatch realInputs, androidx.ink.strokes.StrokeInputBatch predictedInputs);
+ method protected void finalize();
+ method public void finishInput();
+ method public androidx.ink.brush.Brush? getBrush();
+ method @IntRange(from=0L) public int getBrushCoatCount();
+ method @IntRange(from=0L) public int getInputCount();
+ method public boolean getNeedsUpdate();
+ method @IntRange(from=0L) public int getOutlineCount(@IntRange(from=0L) int coatIndex);
+ method @IntRange(from=0L) public int getOutlineVertexCount(@IntRange(from=0L) int coatIndex, @IntRange(from=0L) int outlineIndex);
+ method @IntRange(from=0L) public int getPredictedInputCount();
+ method @IntRange(from=0L) public int getRealInputCount();
+ method public boolean isInputFinished();
+ method public androidx.ink.strokes.StrokeInput populateInput(androidx.ink.strokes.StrokeInput out, @IntRange(from=0L) int index);
+ method public androidx.ink.strokes.MutableStrokeInputBatch populateInputs(androidx.ink.strokes.MutableStrokeInputBatch out);
+ method public androidx.ink.strokes.MutableStrokeInputBatch populateInputs(androidx.ink.strokes.MutableStrokeInputBatch out, optional @IntRange(from=0L) int from);
+ method public androidx.ink.strokes.MutableStrokeInputBatch populateInputs(androidx.ink.strokes.MutableStrokeInputBatch out, optional @IntRange(from=0L) int from, optional @IntRange(from=0L) int to);
+ method public androidx.ink.geometry.BoxAccumulator populateMeshBounds(@IntRange(from=0L) int coatIndex, androidx.ink.geometry.BoxAccumulator outBoxAccumulator);
+ method public void populateOutlinePosition(@IntRange(from=0L) int coatIndex, @IntRange(from=0L) int outlineIndex, @IntRange(from=0L) int outlineVertexIndex, androidx.ink.geometry.MutableVec outPosition);
+ method public androidx.ink.geometry.BoxAccumulator populateUpdatedRegion(androidx.ink.geometry.BoxAccumulator outBoxAccumulator);
+ method public void resetUpdatedRegion();
+ method public void start(androidx.ink.brush.Brush brush);
+ method public androidx.ink.strokes.Stroke toImmutable();
+ method public Object updateShape(optional long currentElapsedTimeMillis);
+ method public void updateShapeOrThrow(optional long currentElapsedTimeMillis);
+ property public final androidx.ink.brush.Brush? brush;
+ field public static final androidx.ink.strokes.InProgressStroke.Companion Companion;
+ }
+
+ public static final class InProgressStroke.Companion {
+ }
+
+ public final class MutableStrokeInputBatch extends androidx.ink.strokes.StrokeInputBatch {
+ ctor public MutableStrokeInputBatch();
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrIgnore(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrIgnore(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis, optional float strokeUnitLengthCm);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrIgnore(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis, optional float strokeUnitLengthCm, optional float pressure);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrIgnore(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrIgnore(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians, optional float orientationRadians);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrIgnore(androidx.ink.strokes.StrokeInput input);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrIgnore(androidx.ink.strokes.StrokeInputBatch inputBatch);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrIgnore(java.util.Collection<androidx.ink.strokes.StrokeInput> inputs);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrThrow(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrThrow(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis, optional float strokeUnitLengthCm);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrThrow(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis, optional float strokeUnitLengthCm, optional float pressure);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrThrow(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrThrow(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians, optional float orientationRadians);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrThrow(androidx.ink.strokes.StrokeInput input);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrThrow(androidx.ink.strokes.StrokeInputBatch inputBatch);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrThrow(java.util.Collection<androidx.ink.strokes.StrokeInput> inputs);
+ method public void clear();
+ }
+
+ public final class Stroke {
+ ctor public Stroke(androidx.ink.brush.Brush brush, androidx.ink.strokes.StrokeInputBatch inputs);
+ ctor public Stroke(androidx.ink.brush.Brush brush, androidx.ink.strokes.StrokeInputBatch inputs, androidx.ink.geometry.PartitionedMesh shape);
+ method public androidx.ink.strokes.Stroke copy(androidx.ink.brush.Brush brush);
+ method protected void finalize();
+ method public androidx.ink.brush.Brush getBrush();
+ method public androidx.ink.strokes.ImmutableStrokeInputBatch getInputs();
+ method public androidx.ink.geometry.PartitionedMesh getShape();
+ property public final androidx.ink.brush.Brush brush;
+ property public final androidx.ink.strokes.ImmutableStrokeInputBatch inputs;
+ property public final androidx.ink.geometry.PartitionedMesh shape;
+ field public static final androidx.ink.strokes.Stroke.Companion Companion;
+ }
+
+ public static final class Stroke.Companion {
+ }
+
+ public final class StrokeInput {
+ ctor public StrokeInput();
+ method @VisibleForTesting public static androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis);
+ method @VisibleForTesting public static androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType);
+ method @VisibleForTesting public static androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm);
+ method @VisibleForTesting public static androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure);
+ method @VisibleForTesting public static androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians);
+ method @VisibleForTesting public static androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians, optional float orientationRadians);
+ method public long getElapsedTimeMillis();
+ method public float getOrientationRadians();
+ method public float getPressure();
+ method public float getStrokeUnitLengthCm();
+ method public float getTiltRadians();
+ method public androidx.ink.brush.InputToolType getToolType();
+ method public float getX();
+ method public float getY();
+ method public boolean hasOrientation();
+ method public boolean hasPressure();
+ method public boolean hasTilt();
+ method public void update(float x, float y, @IntRange(from=0L) long elapsedTimeMillis);
+ method public void update(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType);
+ method public void update(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm);
+ method public void update(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure);
+ method public void update(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians);
+ method public void update(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians, optional float orientationRadians);
+ property public final long elapsedTimeMillis;
+ property public final boolean hasOrientation;
+ property public final boolean hasPressure;
+ property public final boolean hasTilt;
+ property public final float orientationRadians;
+ property public final float pressure;
+ property public final float strokeUnitLengthCm;
+ property public final float tiltRadians;
+ property public final androidx.ink.brush.InputToolType toolType;
+ property public final float x;
+ property public final float y;
+ field public static final androidx.ink.strokes.StrokeInput.Companion Companion;
+ field public static final float NO_ORIENTATION = -1.0f;
+ field public static final float NO_PRESSURE = -1.0f;
+ field public static final float NO_STROKE_UNIT_LENGTH = 0.0f;
+ field public static final float NO_TILT = -1.0f;
+ }
+
+ public static final class StrokeInput.Companion {
+ method @VisibleForTesting public androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis);
+ method @VisibleForTesting public androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType);
+ method @VisibleForTesting public androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm);
+ method @VisibleForTesting public androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure);
+ method @VisibleForTesting public androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians);
+ method @VisibleForTesting public androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians, optional float orientationRadians);
+ }
+
+ public abstract class StrokeInputBatch {
+ method protected final void finalize();
+ method public final operator androidx.ink.strokes.StrokeInput get(int index);
+ method public final long getDurationMillis();
+ method public final int getSize();
+ method public final float getStrokeUnitLengthCm();
+ method public final androidx.ink.brush.InputToolType getToolType();
+ method public final boolean hasOrientation();
+ method public final boolean hasPressure();
+ method public final boolean hasStrokeUnitLength();
+ method public final boolean hasTilt();
+ method public final boolean isEmpty();
+ method public final androidx.ink.strokes.StrokeInput populate(int index, androidx.ink.strokes.StrokeInput outStrokeInput);
+ property public final int size;
+ field public static final androidx.ink.strokes.StrokeInputBatch.Companion Companion;
+ }
+
+ public static final class StrokeInputBatch.Companion {
+ }
+
+}
+
diff --git a/ink/ink-strokes/api/restricted_current.txt b/ink/ink-strokes/api/restricted_current.txt
index e6f50d0..a81e872 100644
--- a/ink/ink-strokes/api/restricted_current.txt
+++ b/ink/ink-strokes/api/restricted_current.txt
@@ -1 +1,157 @@
// Signature format: 4.0
+package androidx.ink.strokes {
+
+ public final class ImmutableStrokeInputBatch extends androidx.ink.strokes.StrokeInputBatch {
+ field public static final androidx.ink.strokes.ImmutableStrokeInputBatch.Companion Companion;
+ field public static final androidx.ink.strokes.ImmutableStrokeInputBatch EMPTY;
+ }
+
+ public static final class ImmutableStrokeInputBatch.Companion {
+ }
+
+ public final class InProgressStroke {
+ ctor public InProgressStroke();
+ method public Object enqueueInputs(androidx.ink.strokes.StrokeInputBatch realInputs, androidx.ink.strokes.StrokeInputBatch predictedInputs);
+ method public void enqueueInputsOrThrow(androidx.ink.strokes.StrokeInputBatch realInputs, androidx.ink.strokes.StrokeInputBatch predictedInputs);
+ method protected void finalize();
+ method public void finishInput();
+ method public androidx.ink.brush.Brush? getBrush();
+ method @IntRange(from=0L) public int getBrushCoatCount();
+ method @IntRange(from=0L) public int getInputCount();
+ method public boolean getNeedsUpdate();
+ method @IntRange(from=0L) public int getOutlineCount(@IntRange(from=0L) int coatIndex);
+ method @IntRange(from=0L) public int getOutlineVertexCount(@IntRange(from=0L) int coatIndex, @IntRange(from=0L) int outlineIndex);
+ method @IntRange(from=0L) public int getPredictedInputCount();
+ method @IntRange(from=0L) public int getRealInputCount();
+ method public boolean isInputFinished();
+ method public androidx.ink.strokes.StrokeInput populateInput(androidx.ink.strokes.StrokeInput out, @IntRange(from=0L) int index);
+ method public androidx.ink.strokes.MutableStrokeInputBatch populateInputs(androidx.ink.strokes.MutableStrokeInputBatch out);
+ method public androidx.ink.strokes.MutableStrokeInputBatch populateInputs(androidx.ink.strokes.MutableStrokeInputBatch out, optional @IntRange(from=0L) int from);
+ method public androidx.ink.strokes.MutableStrokeInputBatch populateInputs(androidx.ink.strokes.MutableStrokeInputBatch out, optional @IntRange(from=0L) int from, optional @IntRange(from=0L) int to);
+ method public androidx.ink.geometry.BoxAccumulator populateMeshBounds(@IntRange(from=0L) int coatIndex, androidx.ink.geometry.BoxAccumulator outBoxAccumulator);
+ method public void populateOutlinePosition(@IntRange(from=0L) int coatIndex, @IntRange(from=0L) int outlineIndex, @IntRange(from=0L) int outlineVertexIndex, androidx.ink.geometry.MutableVec outPosition);
+ method public androidx.ink.geometry.BoxAccumulator populateUpdatedRegion(androidx.ink.geometry.BoxAccumulator outBoxAccumulator);
+ method public void resetUpdatedRegion();
+ method public void start(androidx.ink.brush.Brush brush);
+ method public androidx.ink.strokes.Stroke toImmutable();
+ method public Object updateShape(optional long currentElapsedTimeMillis);
+ method public void updateShapeOrThrow(optional long currentElapsedTimeMillis);
+ property public final androidx.ink.brush.Brush? brush;
+ field public static final androidx.ink.strokes.InProgressStroke.Companion Companion;
+ }
+
+ public static final class InProgressStroke.Companion {
+ }
+
+ public final class MutableStrokeInputBatch extends androidx.ink.strokes.StrokeInputBatch {
+ ctor public MutableStrokeInputBatch();
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrIgnore(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrIgnore(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis, optional float strokeUnitLengthCm);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrIgnore(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis, optional float strokeUnitLengthCm, optional float pressure);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrIgnore(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrIgnore(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians, optional float orientationRadians);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrIgnore(androidx.ink.strokes.StrokeInput input);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrIgnore(androidx.ink.strokes.StrokeInputBatch inputBatch);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrIgnore(java.util.Collection<androidx.ink.strokes.StrokeInput> inputs);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrThrow(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrThrow(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis, optional float strokeUnitLengthCm);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrThrow(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis, optional float strokeUnitLengthCm, optional float pressure);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrThrow(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrThrow(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians, optional float orientationRadians);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrThrow(androidx.ink.strokes.StrokeInput input);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrThrow(androidx.ink.strokes.StrokeInputBatch inputBatch);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrThrow(java.util.Collection<androidx.ink.strokes.StrokeInput> inputs);
+ method public void clear();
+ }
+
+ public final class Stroke {
+ ctor public Stroke(androidx.ink.brush.Brush brush, androidx.ink.strokes.StrokeInputBatch inputs);
+ ctor public Stroke(androidx.ink.brush.Brush brush, androidx.ink.strokes.StrokeInputBatch inputs, androidx.ink.geometry.PartitionedMesh shape);
+ method public androidx.ink.strokes.Stroke copy(androidx.ink.brush.Brush brush);
+ method protected void finalize();
+ method public androidx.ink.brush.Brush getBrush();
+ method public androidx.ink.strokes.ImmutableStrokeInputBatch getInputs();
+ method public androidx.ink.geometry.PartitionedMesh getShape();
+ property public final androidx.ink.brush.Brush brush;
+ property public final androidx.ink.strokes.ImmutableStrokeInputBatch inputs;
+ property public final androidx.ink.geometry.PartitionedMesh shape;
+ field public static final androidx.ink.strokes.Stroke.Companion Companion;
+ }
+
+ public static final class Stroke.Companion {
+ }
+
+ public final class StrokeInput {
+ ctor public StrokeInput();
+ method @VisibleForTesting public static androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis);
+ method @VisibleForTesting public static androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType);
+ method @VisibleForTesting public static androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm);
+ method @VisibleForTesting public static androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure);
+ method @VisibleForTesting public static androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians);
+ method @VisibleForTesting public static androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians, optional float orientationRadians);
+ method public long getElapsedTimeMillis();
+ method public float getOrientationRadians();
+ method public float getPressure();
+ method public float getStrokeUnitLengthCm();
+ method public float getTiltRadians();
+ method public androidx.ink.brush.InputToolType getToolType();
+ method public float getX();
+ method public float getY();
+ method public boolean hasOrientation();
+ method public boolean hasPressure();
+ method public boolean hasTilt();
+ method public void update(float x, float y, @IntRange(from=0L) long elapsedTimeMillis);
+ method public void update(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType);
+ method public void update(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm);
+ method public void update(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure);
+ method public void update(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians);
+ method public void update(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians, optional float orientationRadians);
+ property public final long elapsedTimeMillis;
+ property public final boolean hasOrientation;
+ property public final boolean hasPressure;
+ property public final boolean hasTilt;
+ property public final float orientationRadians;
+ property public final float pressure;
+ property public final float strokeUnitLengthCm;
+ property public final float tiltRadians;
+ property public final androidx.ink.brush.InputToolType toolType;
+ property public final float x;
+ property public final float y;
+ field public static final androidx.ink.strokes.StrokeInput.Companion Companion;
+ field public static final float NO_ORIENTATION = -1.0f;
+ field public static final float NO_PRESSURE = -1.0f;
+ field public static final float NO_STROKE_UNIT_LENGTH = 0.0f;
+ field public static final float NO_TILT = -1.0f;
+ }
+
+ public static final class StrokeInput.Companion {
+ method @VisibleForTesting public androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis);
+ method @VisibleForTesting public androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType);
+ method @VisibleForTesting public androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm);
+ method @VisibleForTesting public androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure);
+ method @VisibleForTesting public androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians);
+ method @VisibleForTesting public androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians, optional float orientationRadians);
+ }
+
+ public abstract class StrokeInputBatch {
+ method protected final void finalize();
+ method public final operator androidx.ink.strokes.StrokeInput get(int index);
+ method public final long getDurationMillis();
+ method public final int getSize();
+ method public final float getStrokeUnitLengthCm();
+ method public final androidx.ink.brush.InputToolType getToolType();
+ method public final boolean hasOrientation();
+ method public final boolean hasPressure();
+ method public final boolean hasStrokeUnitLength();
+ method public final boolean hasTilt();
+ method public final boolean isEmpty();
+ method public final androidx.ink.strokes.StrokeInput populate(int index, androidx.ink.strokes.StrokeInput outStrokeInput);
+ property public final int size;
+ field public static final androidx.ink.strokes.StrokeInputBatch.Companion Companion;
+ }
+
+ public static final class StrokeInputBatch.Companion {
+ }
+
+}
+
diff --git a/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/InProgressStroke.kt b/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/InProgressStroke.kt
index 6bfd24e..08b1649 100644
--- a/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/InProgressStroke.kt
+++ b/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/InProgressStroke.kt
@@ -47,7 +47,6 @@
* 6. For best performance, reuse this object and go back to step 1 rather than allocating a new
* instance.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
@Suppress("NotCloseable") // Finalize is only used to free the native peer.
public class InProgressStroke {
@@ -251,6 +250,7 @@
* Writes to [outBoxAccumulator] the bounding box of the vertex positions of the mesh for brush
* coat [coatIndex].
*
+ * @param coatIndex The index of the coat to obtain the bounding box from.
* @param outBoxAccumulator The pre-allocated [BoxAccumulator] to be filled with the result.
*/
public fun populateMeshBounds(
diff --git a/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/Stroke.kt b/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/Stroke.kt
index 050e442..72c67e7 100644
--- a/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/Stroke.kt
+++ b/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/Stroke.kt
@@ -33,7 +33,6 @@
* [InProgressStrokesView] or [InProgressStroke], which will ultimately return a [Stroke] when input
* is completed.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
@OptIn(ExperimentalInkCustomBrushApi::class)
@Suppress("NotCloseable") // Finalize is only used to free the native peer.
public class Stroke {
@@ -125,8 +124,8 @@
*/
internal constructor(nativeAddress: Long, brush: Brush) {
val shape = PartitionedMesh(StrokeJni.allocShallowCopyOfShape(nativeAddress))
- require(shape.renderGroupCount == brush.family.coats.size) {
- "The shape must have one render group per brush coat, but found ${shape.renderGroupCount} render groups in shape and ${brush.family.coats.size} brush coats in brush."
+ require(shape.getRenderGroupCount() == brush.family.coats.size) {
+ "The shape must have one render group per brush coat, but found ${shape.getRenderGroupCount()} render groups in shape and ${brush.family.coats.size} brush coats in brush."
}
this.nativeAddress = nativeAddress
this.brush = brush
@@ -142,8 +141,8 @@
* [PartitionedMesh] is being stored in addition to the [Brush] and [StrokeInputBatch].
*/
public constructor(brush: Brush, inputs: StrokeInputBatch, shape: PartitionedMesh) {
- require(shape.renderGroupCount == brush.family.coats.size) {
- "The shape must have one render group per brush coat, but found ${shape.renderGroupCount} render groups in shape and ${brush.family.coats.size} brush coats in brush."
+ require(shape.getRenderGroupCount() == brush.family.coats.size) {
+ "The shape must have one render group per brush coat, but found ${shape.getRenderGroupCount()} render groups in shape and ${brush.family.coats.size} brush coats in brush."
}
this.brush = brush
this.shape = shape
diff --git a/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/StrokeInput.kt b/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/StrokeInput.kt
index f8abb1e..67f0e3d 100644
--- a/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/StrokeInput.kt
+++ b/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/StrokeInput.kt
@@ -31,7 +31,6 @@
* ones which could introduce unpredictable garbage collection related delays to the time-sensitive
* input path. This class has the [update] method for that purpose, rather than being immutable.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public class StrokeInput {
/** The x-coordinate of the input position in stroke space. */
public var x: Float = 0F
@@ -143,6 +142,12 @@
* @param elapsedTimeMillis Marks the number of milliseconds since the stroke started. It is a
* non-negative timestamp in the [android.os.SystemClock.elapsedRealtime] time base.
* @param toolType The type of tool used to create this input data.
+ * @param strokeUnitLengthCm The physical distance in centimeters that the pointer must travel
+ * in order to produce an input motion of one stroke unit. For stylus/touch, this is the
+ * real-world distance that the stylus/ fingertip must move in physical space; for mouse, this
+ * is the visual distance that the mouse pointer must travel along the surface of the display.
+ * A value of [NO_STROKE_UNIT_LENGTH] indicates that the relationship between stroke space and
+ * physical space is unknown or ill-defined.
* @param pressure Should be within [0, 1] but it's not enforced until added to a
* [StrokeInputBatch] object. Absence of [pressure] data is represented with [NO_PRESSURE].
* @param tiltRadians The angle in radians between a stylus and the line perpendicular to the
diff --git a/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/StrokeInputBatch.kt b/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/StrokeInputBatch.kt
index 4f4e2a8..f35fe70 100644
--- a/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/StrokeInputBatch.kt
+++ b/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/StrokeInputBatch.kt
@@ -29,7 +29,6 @@
* change, and a [MutableStrokeInputBatch] for data that is meant to be modified or incrementally
* built.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
@Suppress("NotCloseable") // Finalize is only used to free the native peer.
public abstract class StrokeInputBatch internal constructor(nativePointer: Long) {
@@ -132,7 +131,6 @@
* An immutable implementation of [StrokeInputBatch]. For a mutable alternative, see
* [MutableStrokeInputBatch].
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public class ImmutableStrokeInputBatch
/**
* Constructor for Kotlin [ImmutableStrokeInputBatch] objects that are originally created in native
@@ -177,7 +175,6 @@
* [0, 2π) or be [StrokeInput.NO_ORIENTATION].
* 7) The [toolType] and [strokeUnitLengthCm] values must be the same across all inputs.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public class MutableStrokeInputBatch : StrokeInputBatch(StrokeInputBatchNative.createNativePeer()) {
public fun clear(): Unit = MutableStrokeInputBatchNative.clear(nativePointer)
diff --git a/ink/ink-strokes/src/jvmAndroidTest/kotlin/androidx/ink/strokes/StrokeTest.kt b/ink/ink-strokes/src/jvmAndroidTest/kotlin/androidx/ink/strokes/StrokeTest.kt
index c730120..2155aff 100644
--- a/ink/ink-strokes/src/jvmAndroidTest/kotlin/androidx/ink/strokes/StrokeTest.kt
+++ b/ink/ink-strokes/src/jvmAndroidTest/kotlin/androidx/ink/strokes/StrokeTest.kt
@@ -68,7 +68,7 @@
// Create a [ModeledShape] with render group.
val inputs = makeTestInputs()
val shape = Stroke(buildTestBrush(), inputs).shape
- assertThat(shape.renderGroupCount).isEqualTo(1)
+ assertThat(shape.getRenderGroupCount()).isEqualTo(1)
// Create a brush with two brush coats.
val coat = BrushCoat(BrushTip(), BrushPaint())
diff --git a/inspection/inspection-gradle-plugin/lint-baseline.xml b/inspection/inspection-gradle-plugin/lint-baseline.xml
index 603e7bd..ef0ffef 100644
--- a/inspection/inspection-gradle-plugin/lint-baseline.xml
+++ b/inspection/inspection-gradle-plugin/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.6.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.6.0-beta01)" variant="all" version="8.6.0-beta01">
+<issues format="6" by="lint 8.7.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.7.0-alpha02)" variant="all" version="8.7.0-alpha02">
<issue
id="EagerGradleConfiguration"
@@ -38,6 +38,24 @@
</issue>
<issue
+ id="GradleProjectIsolation"
+ message="Avoid using method findProject"
+ errorLine1=" val inspectorProject = libraryProject.rootProject.findProject(inspectorProjectPath)"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/kotlin/androidx/inspection/gradle/InspectionPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" val inspectorProject = libraryProject.rootProject.findProject(inspectorProjectPath)"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/kotlin/androidx/inspection/gradle/InspectionPlugin.kt"/>
+ </issue>
+
+ <issue
id="WithPluginClasspathUsage"
message="Avoid usage of GradleRunner#withPluginClasspath, which is broken. Instead use something like https://github.com/autonomousapps/dependency-analysis-gradle-plugin/tree/main/testkit#gradle-testkit-support-plugin"
errorLine1=" GradleRunner.create().withProjectDir(projectSetup.rootDir).withPluginClasspath()"
diff --git a/leanback/leanback-grid/build.gradle b/leanback/leanback-grid/build.gradle
index f24c785..30fc5af 100644
--- a/leanback/leanback-grid/build.gradle
+++ b/leanback/leanback-grid/build.gradle
@@ -44,8 +44,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(project(":internal-testutils-espresso"))
androidTestImplementation(project(":internal-testutils-runtime"))
androidTestImplementation(project(":internal-testutils-common"))
diff --git a/leanback/leanback-paging/build.gradle b/leanback/leanback-paging/build.gradle
index 9eade4e..9e1cfb0 100644
--- a/leanback/leanback-paging/build.gradle
+++ b/leanback/leanback-paging/build.gradle
@@ -27,8 +27,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(project(":internal-testutils-espresso"))
androidTestImplementation(project(":internal-testutils-runtime"))
androidTestImplementation(project(":internal-testutils-common"))
diff --git a/leanback/leanback-tab/build.gradle b/leanback/leanback-tab/build.gradle
index 78d8b76..74ab4ba 100644
--- a/leanback/leanback-tab/build.gradle
+++ b/leanback/leanback-tab/build.gradle
@@ -26,8 +26,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(project(":internal-testutils-espresso"))
androidTestImplementation(project(":internal-testutils-runtime"))
androidTestImplementation(project(":internal-testutils-common"))
diff --git a/leanback/leanback/build.gradle b/leanback/leanback/build.gradle
index 5f18f6e..ed754ce 100644
--- a/leanback/leanback/build.gradle
+++ b/leanback/leanback/build.gradle
@@ -31,8 +31,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(project(":internal-testutils-espresso"))
androidTestImplementation(project(":internal-testutils-runtime"))
androidTestImplementation(project(":internal-testutils-common"))
diff --git a/libraryversions.toml b/libraryversions.toml
index 3980454..75a598d 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -1,6 +1,6 @@
[versions]
ACTIVITY = "1.10.0-alpha02"
-ANNOTATION = "1.9.0-alpha02"
+ANNOTATION = "1.9.0-alpha03"
ANNOTATION_EXPERIMENTAL = "1.5.0-alpha01"
APPCOMPAT = "1.8.0-alpha01"
APPSEARCH = "1.1.0-alpha05"
@@ -89,9 +89,9 @@
LEANBACK_TAB = "1.1.0-beta01"
LEGACY = "1.1.0-alpha01"
LIBYUV = "0.1.0-dev01"
-LIFECYCLE = "2.9.0-alpha01"
+LIFECYCLE = "2.9.0-alpha02"
LIFECYCLE_EXTENSIONS = "2.2.0"
-LINT = "1.0.0-alpha01"
+LINT = "1.0.0-alpha02"
LOADER = "1.2.0-alpha01"
MEDIA = "1.7.0-rc01"
MEDIAROUTER = "1.8.0-alpha01"
diff --git a/lifecycle/lifecycle-runtime-compose/build.gradle b/lifecycle/lifecycle-runtime-compose/build.gradle
index 0ffdf85..f15ee49 100644
--- a/lifecycle/lifecycle-runtime-compose/build.gradle
+++ b/lifecycle/lifecycle-runtime-compose/build.gradle
@@ -41,7 +41,7 @@
sourceSets {
commonMain {
dependencies {
- api(projectOrArtifact(":lifecycle:lifecycle-runtime"))
+ api(project(":lifecycle:lifecycle-runtime"))
api("androidx.annotation:annotation:1.8.1")
api(project(":compose:runtime:runtime"))
}
@@ -54,14 +54,14 @@
// `lifecycle-runtime-compose` also updates `lifecycle-runtime-ktx`
// in cases where our constraints fail (e.g., internally in AndroidX
// when using project dependencies).
- api(projectOrArtifact(":lifecycle:lifecycle-runtime-ktx"))
+ api(project(":lifecycle:lifecycle-runtime-ktx"))
}
}
androidInstrumentedTest {
dependencies {
- implementation(projectOrArtifact(":lifecycle:lifecycle-runtime-testing"))
- implementation(projectOrArtifact(":compose:ui:ui-test"))
+ implementation(project(":lifecycle:lifecycle-runtime-testing"))
+ implementation(project(":compose:ui:ui-test"))
implementation(project(":compose:test-utils"))
implementation(libs.testRules)
implementation(libs.testRunner)
diff --git a/lifecycle/lifecycle-runtime-compose/samples/build.gradle b/lifecycle/lifecycle-runtime-compose/samples/build.gradle
index 40a0c51..c352425 100644
--- a/lifecycle/lifecycle-runtime-compose/samples/build.gradle
+++ b/lifecycle/lifecycle-runtime-compose/samples/build.gradle
@@ -35,7 +35,7 @@
compileOnly(project(":annotation:annotation-sampled"))
implementation(libs.kotlinStdlib)
- implementation projectOrArtifact(":lifecycle:lifecycle-runtime-compose")
+ implementation project(":lifecycle:lifecycle-runtime-compose")
implementation "androidx.compose.material:material:1.0.1"
}
diff --git a/lifecycle/lifecycle-runtime-testing-lint/build.gradle b/lifecycle/lifecycle-runtime-testing-lint/build.gradle
index 9a253fb..002ec45 100644
--- a/lifecycle/lifecycle-runtime-testing-lint/build.gradle
+++ b/lifecycle/lifecycle-runtime-testing-lint/build.gradle
@@ -35,9 +35,9 @@
compileOnly(libs.androidLintMinApi)
compileOnly(libs.kotlinStdlib)
// Needed for Compose lint util functions
- bundleInside(projectOrArtifact(":compose:lint:common"))
+ bundleInside(project(":compose:lint:common"))
- testImplementation(projectOrArtifact(":compose:lint:common-test"))
+ testImplementation(project(":compose:lint:common-test"))
testImplementation(libs.kotlinStdlib)
testImplementation(libs.kotlinReflect)
testImplementation(libs.kotlinStdlibJdk8)
diff --git a/lifecycle/lifecycle-viewmodel-compose-lint/build.gradle b/lifecycle/lifecycle-viewmodel-compose-lint/build.gradle
index ab1344d..4cceb15 100644
--- a/lifecycle/lifecycle-viewmodel-compose-lint/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-compose-lint/build.gradle
@@ -35,9 +35,9 @@
dependencies {
compileOnly(libs.androidLintMinApi)
compileOnly(libs.kotlinStdlib)
- bundleInside(projectOrArtifact(":compose:lint:common"))
+ bundleInside(project(":compose:lint:common"))
- testImplementation(projectOrArtifact(":compose:lint:common-test"))
+ testImplementation(project(":compose:lint:common-test"))
testImplementation(libs.kotlinStdlib)
testImplementation(libs.kotlinReflect)
testImplementation(libs.kotlinStdlibJdk8)
diff --git a/lifecycle/lifecycle-viewmodel-compose/build.gradle b/lifecycle/lifecycle-viewmodel-compose/build.gradle
index 13aebe3..b99d1b3 100644
--- a/lifecycle/lifecycle-viewmodel-compose/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-compose/build.gradle
@@ -39,8 +39,8 @@
sourceSets {
commonMain {
dependencies {
- api(projectOrArtifact(":lifecycle:lifecycle-common"))
- api(projectOrArtifact(":lifecycle:lifecycle-viewmodel"))
+ api(project(":lifecycle:lifecycle-common"))
+ api(project(":lifecycle:lifecycle-viewmodel"))
api("androidx.annotation:annotation:1.8.1")
api("androidx.compose.runtime:runtime:1.6.0")
implementation(libs.kotlinStdlib)
@@ -54,20 +54,20 @@
androidMain {
dependsOn(commonMain)
dependencies {
- api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-savedstate"))
+ api(project(":lifecycle:lifecycle-viewmodel-savedstate"))
api("androidx.compose.ui:ui:1.6.0")
// Converting `lifecycle-viewmodel-compose` to KMP and including a transitive
// dependency on `lifecycle-livedata-core` triggered a Gradle bug. Adding the
// `livedata` dependency directly works around the issue.
// See https://github.com/gradle/gradle/issues/14220 for details.
- compileOnly(projectOrArtifact(":lifecycle:lifecycle-livedata-core"))
+ compileOnly(project(":lifecycle:lifecycle-livedata-core"))
}
}
androidInstrumentedTest {
dependsOn(commonTest)
dependencies {
- implementation(projectOrArtifact(":compose:ui:ui-test-junit4"))
+ implementation(project(":compose:ui:ui-test-junit4"))
implementation(project(":compose:test-utils"))
implementation(libs.testRules)
implementation(libs.testRunner)
@@ -80,10 +80,10 @@
// Outside of androidx this is resolved via constraint added to lifecycle-common,
// but it doesn't work in androidx.
// See aosp/1804059
- implementation(projectOrArtifact(":lifecycle:lifecycle-common-java8"))
- implementation(projectOrArtifact(":lifecycle:lifecycle-viewmodel-savedstate"))
- implementation(projectOrArtifact(":activity:activity-compose"))
- implementation(projectOrArtifact(":lifecycle:lifecycle-runtime-testing"))
+ implementation(project(":lifecycle:lifecycle-common-java8"))
+ implementation(project(":lifecycle:lifecycle-viewmodel-savedstate"))
+ implementation(project(":activity:activity-compose"))
+ implementation(project(":lifecycle:lifecycle-runtime-testing"))
}
}
}
@@ -100,7 +100,7 @@
description = "Compose integration with Lifecycle ViewModel"
legacyDisableKotlinStrictApiMode = true
metalavaK2UastEnabled = false
- samples(projectOrArtifact(":lifecycle:lifecycle-viewmodel-compose:lifecycle-viewmodel-compose-samples"))
+ samples(project(":lifecycle:lifecycle-viewmodel-compose:lifecycle-viewmodel-compose-samples"))
}
android {
diff --git a/lifecycle/lifecycle-viewmodel-compose/samples/build.gradle b/lifecycle/lifecycle-viewmodel-compose/samples/build.gradle
index aecf4ad..a9fb65d 100644
--- a/lifecycle/lifecycle-viewmodel-compose/samples/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-compose/samples/build.gradle
@@ -34,9 +34,9 @@
dependencies {
compileOnly(project(":annotation:annotation-sampled"))
implementation(libs.kotlinStdlib)
- implementation projectOrArtifact(":lifecycle:lifecycle-common-java8")
- implementation projectOrArtifact(":lifecycle:lifecycle-viewmodel-compose")
- implementation projectOrArtifact(":lifecycle:lifecycle-viewmodel-savedstate")
+ implementation project(":lifecycle:lifecycle-common-java8")
+ implementation project(":lifecycle:lifecycle-viewmodel-compose")
+ implementation project(":lifecycle:lifecycle-viewmodel-savedstate")
}
androidx {
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/build.gradle b/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
index 6f417dd..108a647 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
@@ -40,13 +40,13 @@
api("androidx.annotation:annotation:1.8.1")
api("androidx.core:core-ktx:1.2.0")
api("androidx.savedstate:savedstate:1.2.1")
- api(projectOrArtifact(":lifecycle:lifecycle-livedata-core"))
- api(projectOrArtifact(":lifecycle:lifecycle-viewmodel"))
+ api(project(":lifecycle:lifecycle-livedata-core"))
+ api(project(":lifecycle:lifecycle-viewmodel"))
api(libs.kotlinStdlib)
api(libs.kotlinCoroutinesAndroid)
- androidTestImplementation projectOrArtifact(":lifecycle:lifecycle-runtime")
- androidTestImplementation projectOrArtifact(":lifecycle:lifecycle-livedata-core")
+ androidTestImplementation project(":lifecycle:lifecycle-runtime")
+ androidTestImplementation project(":lifecycle:lifecycle-livedata-core")
androidTestImplementation ("androidx.fragment:fragment:1.3.0")
androidTestImplementation project(":internal-testutils-runtime")
androidTestImplementation(libs.truth)
diff --git a/lint/lint-gradle/src/main/java/androidx/lint/gradle/DiscouragedGradleMethodDetector.kt b/lint/lint-gradle/src/main/java/androidx/lint/gradle/DiscouragedGradleMethodDetector.kt
index e8680ae..3f129b8 100644
--- a/lint/lint-gradle/src/main/java/androidx/lint/gradle/DiscouragedGradleMethodDetector.kt
+++ b/lint/lint-gradle/src/main/java/androidx/lint/gradle/DiscouragedGradleMethodDetector.kt
@@ -140,8 +140,11 @@
Replacement(NAMED_DOMAIN_OBJECT_COLLECTION, null, EAGER_CONFIGURATION_ISSUE),
"findByName" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
"findByPath" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
+ "findProject" to Replacement(PROJECT, null, PROJECT_ISOLATION_ISSUE),
"findProperty" to
Replacement(PROJECT, "providers.gradleProperty", PROJECT_ISOLATION_ISSUE),
+ "hasProperty" to
+ Replacement(PROJECT, "providers.gradleProperty", PROJECT_ISOLATION_ISSUE),
"property" to
Replacement(PROJECT, "providers.gradleProperty", PROJECT_ISOLATION_ISSUE),
"iterator" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
@@ -149,8 +152,10 @@
"getAt" to Replacement(TASK_COLLECTION, "named", EAGER_CONFIGURATION_ISSUE),
"getByPath" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
"getByName" to Replacement(TASK_CONTAINER, "named", EAGER_CONFIGURATION_ISSUE),
+ "getParent" to Replacement(PROJECT, null, PROJECT_ISOLATION_ISSUE),
"getProperties" to
Replacement(PROJECT, "providers.gradleProperty", PROJECT_ISOLATION_ISSUE),
+ "getRootProject" to Replacement(PROJECT, null, PROJECT_ISOLATION_ISSUE),
"matching" to Replacement(TASK_COLLECTION, null, EAGER_CONFIGURATION_ISSUE),
"replace" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
"remove" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
diff --git a/loader/loader-ktx/build.gradle b/loader/loader-ktx/build.gradle
index ffb4403..d411d1b 100644
--- a/loader/loader-ktx/build.gradle
+++ b/loader/loader-ktx/build.gradle
@@ -43,8 +43,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
}
androidx {
diff --git a/loader/loader/build.gradle b/loader/loader/build.gradle
index 706fba2..209c8fb 100644
--- a/loader/loader/build.gradle
+++ b/loader/loader/build.gradle
@@ -15,7 +15,7 @@
dependencies {
api("androidx.annotation:annotation:1.8.1")
api("androidx.lifecycle:lifecycle-viewmodel:2.0.0")
- implementation(projectOrArtifact(":core:core"))
+ implementation(project(":core:core"))
implementation("androidx.collection:collection:1.4.2")
implementation("androidx.lifecycle:lifecycle-livedata-core:2.0.0")
@@ -25,8 +25,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
}
androidx {
diff --git a/navigation/integration-tests/testapp/build.gradle b/navigation/integration-tests/testapp/build.gradle
index 4398e0c..030aecf 100644
--- a/navigation/integration-tests/testapp/build.gradle
+++ b/navigation/integration-tests/testapp/build.gradle
@@ -23,8 +23,8 @@
dependencies {
implementation(libs.kotlinStdlib)
implementation("androidx.appcompat:appcompat:1.1.0")
- api(projectOrArtifact(":fragment:fragment-ktx"))
- api(projectOrArtifact(":transition:transition-ktx"))
+ api(project(":fragment:fragment-ktx"))
+ api(project(":transition:transition-ktx"))
implementation(project(":navigation:navigation-fragment-ktx"))
implementation(project(":navigation:navigation-ui-ktx"))
implementation(project(":internal-testutils-navigation"), {
diff --git a/navigation/navigation-common-lint/build.gradle b/navigation/navigation-common-lint/build.gradle
index d1f2801..66c71e5 100644
--- a/navigation/navigation-common-lint/build.gradle
+++ b/navigation/navigation-common-lint/build.gradle
@@ -35,7 +35,7 @@
dependencies {
compileOnly(libs.kotlinStdlib)
compileOnly(libs.androidLintApi)
- bundleInside(projectOrArtifact(":navigation:navigation-lint-common"))
+ bundleInside(project(":navigation:navigation-lint-common"))
testImplementation(libs.kotlinStdlib)
testImplementation(libs.androidLint)
diff --git a/navigation/navigation-common/build.gradle b/navigation/navigation-common/build.gradle
index bcde5af..cc8206c 100644
--- a/navigation/navigation-common/build.gradle
+++ b/navigation/navigation-common/build.gradle
@@ -67,8 +67,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.espressoCore)
androidTestImplementation(libs.truth)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.kotlinStdlib)
lintPublish(project(':navigation:navigation-common-lint'))
diff --git a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationAndroidTest.kt b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationAndroidTest.kt
index 897a60c..10b89a5 100644
--- a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationAndroidTest.kt
+++ b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationAndroidTest.kt
@@ -68,7 +68,7 @@
destination.id = 1
assertThat(destination.route).isEqualTo("route")
assertThat(destination.id).isEqualTo(1)
- assertThat(destination.hasDeepLink(createRoute("route").toUri())).isTrue()
+ assertThat(destination.hasDeepLink(createRoute("route").toUri())).isFalse()
destination.route = null
assertThat(destination.route).isNull()
@@ -682,4 +682,44 @@
assertThat(destination.hasRoute<TestClass>()).isFalse()
}
+
+ @Test
+ fun routeNotAddedToDeepLink() {
+ val destination = NoOpNavigator().createDestination()
+ assertThat(destination.route).isNull()
+
+ destination.route = "route"
+ assertThat(destination.route).isEqualTo("route")
+ assertThat(destination.hasDeepLink(createRoute("route").toUri())).isFalse()
+ }
+
+ @Test
+ fun matchRoute() {
+ val destination = NoOpNavigator().createDestination()
+
+ destination.route = "route"
+ assertThat(destination.route).isEqualTo("route")
+
+ val match = destination.matchRoute("route")
+ assertThat(match).isNotNull()
+ assertThat(match!!.destination).isEqualTo(destination)
+ }
+
+ @Test
+ fun matchRouteAfterSetNewRoute() {
+ val destination = NoOpNavigator().createDestination()
+
+ destination.route = "route"
+ assertThat(destination.route).isEqualTo("route")
+
+ val match = destination.matchRoute("route")
+ assertThat(match).isNotNull()
+ assertThat(match!!.destination).isEqualTo(destination)
+
+ destination.route = "newRoute"
+ assertThat(destination.route).isEqualTo("newRoute")
+ val match2 = destination.matchRoute("newRoute")
+ assertThat(match2).isNotNull()
+ assertThat(match2!!.destination).isEqualTo(destination)
+ }
}
diff --git a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationBuilderTest.kt b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationBuilderTest.kt
index 5e1c141..64768f7 100644
--- a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationBuilderTest.kt
+++ b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationBuilderTest.kt
@@ -168,9 +168,7 @@
}
assertThat(expected.message)
.isEqualTo(
- "Deep link android-app://androidx.navigation/route can't be used to " +
- "open destination NavDestination(0xa2bd82dc).\n" +
- "Following required arguments are missing: [intArg]"
+ "Cannot set route \"route\" for destination NavDestination(0x0). Following required arguments are missing: [intArg]"
)
}
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt
index de01d95..4225f41 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt
@@ -223,14 +223,36 @@
id = 0
} else {
require(route.isNotBlank()) { "Cannot have an empty route" }
- val internalRoute = createRoute(route)
- id = internalRoute.hashCode()
- addDeepLink(internalRoute)
+
+ // make sure the route contains all required arguments
+ val tempRoute = createRoute(route)
+ val tempDeepLink = NavDeepLink.Builder().setUriPattern(tempRoute).build()
+ val missingRequiredArguments =
+ _arguments.missingRequiredArguments { key ->
+ key !in tempDeepLink.argumentsNames
+ }
+ require(missingRequiredArguments.isEmpty()) {
+ "Cannot set route \"$route\" for destination $this. " +
+ "Following required arguments are missing: $missingRequiredArguments"
+ }
+
+ routeDeepLink = lazy { NavDeepLink.Builder().setUriPattern(tempRoute).build() }
+ id = tempRoute.hashCode()
}
- deepLinks.remove(deepLinks.firstOrNull { it.uriPattern == createRoute(field) })
field = route
}
+ /**
+ * This destination's unique route as a NavDeepLink.
+ *
+ * This deeplink must be kept private and segregated from the explicitly added public deeplinks
+ * to ensure that external users cannot deeplink into this destination with this routeDeepLink.
+ *
+ * This value is reassigned a new lazy value every time [route] is updated to ensure that any
+ * initialized lazy value is overwritten with the latest value.
+ */
+ private var routeDeepLink: Lazy<NavDeepLink>? = null
+
public open val displayName: String
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) get() = idName ?: id.toString()
@@ -346,26 +368,28 @@
}
/**
- * Determines if this NavDestination has a deep link of this route.
+ * Determines if this NavDestination's route matches the requested route.
*
* @param [route] The route to match against this [NavDestination.route]
* @return The matching [DeepLinkMatch], or null if no match was found.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public fun matchDeepLink(route: String): DeepLinkMatch? {
- val request = NavDeepLinkRequest.Builder.fromUri(createRoute(route).toUri()).build()
- val matchingDeepLink =
- if (this is NavGraph) {
- matchDeepLinkComprehensive(
- request,
- searchChildren = false,
- searchParent = false,
- lastVisited = this
- )
- } else {
- matchDeepLink(request)
- }
- return matchingDeepLink
+ public fun matchRoute(route: String): DeepLinkMatch? {
+ val routeDeepLink = this.routeDeepLink?.value ?: return null
+
+ val uri = createRoute(route).toUri()
+
+ // includes matching args for path, query, and fragment
+ val matchingArguments = routeDeepLink.getMatchingArguments(uri, _arguments) ?: return null
+ val matchingPathSegments = routeDeepLink.calculateMatchingPathSegments(uri)
+ return DeepLinkMatch(
+ this,
+ matchingArguments,
+ routeDeepLink.isExactDeepLink,
+ matchingPathSegments,
+ false,
+ -1
+ )
}
/**
@@ -481,7 +505,7 @@
// if no match based on routePattern, this means route contains filled in args or query
// params
- val matchingDeepLink = matchDeepLink(route)
+ val matchingDeepLink = matchRoute(route)
// if no matchingDeepLink or mismatching destination, return false directly
if (this != matchingDeepLink?.destination) return false
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt
index 3a34696..5fbe69d 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt
@@ -65,6 +65,50 @@
}
/**
+ * Matches route with all children and parents recursively.
+ *
+ * Does not revisit graphs (whether it's a child or parent) if it has already been visited.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public fun matchRouteComprehensive(
+ route: String,
+ searchChildren: Boolean,
+ searchParent: Boolean,
+ lastVisited: NavDestination
+ ): DeepLinkMatch? {
+ // First try to match with this graph's route
+ val bestMatch = matchRoute(route)
+ // If searchChildren is true, search through all child destinations for a matching route
+ val bestChildMatch =
+ if (searchChildren) {
+ mapNotNull { child ->
+ when (child) {
+ lastVisited -> null
+ is NavGraph ->
+ child.matchRouteComprehensive(
+ route,
+ searchChildren = true,
+ searchParent = false,
+ lastVisited = this
+ )
+ else -> child.matchRoute(route)
+ }
+ }
+ .maxOrNull()
+ } else null
+
+ // If searchParent is true, search through all parents (and their children) destinations
+ // for a matching route
+ val bestParentMatch =
+ parent?.let {
+ if (searchParent && it != lastVisited)
+ it.matchRouteComprehensive(route, searchChildren, true, this)
+ else null
+ }
+ return listOfNotNull(bestMatch, bestChildMatch, bestParentMatch).maxOrNull()
+ }
+
+ /**
* Matches deeplink with all children and parents recursively.
*
* Does not revisit graphs (whether it's a child or parent) if it has already been visited.
@@ -262,7 +306,7 @@
nodes.valueIterator().asSequence().firstOrNull {
// first try matching with routePattern
// if not found with routePattern, try matching with route args
- it.route.equals(route) || it.matchDeepLink(route) != null
+ it.route.equals(route) || it.matchRoute(route) != null
}
// Search the parent for the NavDestination if it is not a child of this navigation graph
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraphNavigator.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraphNavigator.kt
index 79035974..a8b4a4f 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraphNavigator.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraphNavigator.kt
@@ -84,7 +84,7 @@
)
}
if (startRoute != null && startRoute != startDestination.route) {
- val matchingArgs = startDestination.matchDeepLink(startRoute)?.matchingArgs
+ val matchingArgs = startDestination.matchRoute(startRoute)?.matchingArgs
if (matchingArgs != null && !matchingArgs.isEmpty) {
val bundle = Bundle()
// we need to add args from startRoute, but it should not override existing args
diff --git a/navigation/navigation-compose-lint/build.gradle b/navigation/navigation-compose-lint/build.gradle
index 4548102..23b3515 100644
--- a/navigation/navigation-compose-lint/build.gradle
+++ b/navigation/navigation-compose-lint/build.gradle
@@ -34,10 +34,10 @@
dependencies {
compileOnly(libs.androidLintMinApi)
compileOnly(libs.kotlinStdlib)
- bundleInside(projectOrArtifact(":compose:lint:common"))
- bundleInside(projectOrArtifact(":navigation:navigation-lint-common"))
+ bundleInside(project(":compose:lint:common"))
+ bundleInside(project(":navigation:navigation-lint-common"))
- testImplementation(projectOrArtifact(":compose:lint:common-test"))
+ testImplementation(project(":compose:lint:common-test"))
testImplementation(libs.kotlinStdlib)
testImplementation(libs.kotlinReflect)
testImplementation(libs.kotlinStdlibJdk8)
diff --git a/navigation/navigation-compose/build.gradle b/navigation/navigation-compose/build.gradle
index 0088065..af843ef 100644
--- a/navigation/navigation-compose/build.gradle
+++ b/navigation/navigation-compose/build.gradle
@@ -36,30 +36,30 @@
api("androidx.compose.runtime:runtime-saveable:1.7.0-rc01")
api("androidx.compose.ui:ui:1.7.0-rc01")
api("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
- api(projectOrArtifact(":navigation:navigation-runtime-ktx"))
+ api(project(":navigation:navigation-runtime-ktx"))
implementation(libs.kotlinSerializationCore)
- androidTestImplementation(projectOrArtifact(":compose:material:material"))
+ androidTestImplementation(project(":compose:material:material"))
androidTestImplementation project(":compose:test-utils")
- androidTestImplementation projectOrArtifact(":compose:ui:ui-tooling")
- androidTestImplementation(projectOrArtifact(":navigation:navigation-testing"))
- androidTestImplementation(projectOrArtifact(":internal-testutils-navigation"), {
+ androidTestImplementation project(":compose:ui:ui-tooling")
+ androidTestImplementation(project(":navigation:navigation-testing"))
+ androidTestImplementation(project(":internal-testutils-navigation"), {
exclude group: "androidx.navigation", module: "navigation-common"
})
- androidTestImplementation(projectOrArtifact(":compose:ui:ui-test-junit4"))
- androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-common"))
- androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-common-java8"))
- androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-livedata-core"))
- androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-viewmodel"))
- androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-viewmodel-savedstate"))
- androidTestImplementation(projectOrArtifact(":activity:activity-ktx"))
+ androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+ androidTestImplementation(project(":lifecycle:lifecycle-common"))
+ androidTestImplementation(project(":lifecycle:lifecycle-common-java8"))
+ androidTestImplementation(project(":lifecycle:lifecycle-livedata-core"))
+ androidTestImplementation(project(":lifecycle:lifecycle-viewmodel"))
+ androidTestImplementation(project(":lifecycle:lifecycle-viewmodel-savedstate"))
+ androidTestImplementation(project(":activity:activity-ktx"))
androidTestImplementation("androidx.collection:collection-ktx:1.4.2")
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.junit)
androidTestImplementation(libs.truth)
- lintChecks(projectOrArtifact(":navigation:navigation-compose-lint"))
- lintPublish(projectOrArtifact(":navigation:navigation-compose-lint"))
+ lintChecks(project(":navigation:navigation-compose-lint"))
+ lintPublish(project(":navigation:navigation-compose-lint"))
}
androidx {
@@ -68,7 +68,7 @@
inceptionYear = "2020"
description = "Compose integration with Navigation"
legacyDisableKotlinStrictApiMode = true
- samples(projectOrArtifact(":navigation:navigation-compose:navigation-compose-samples"))
+ samples(project(":navigation:navigation-compose:navigation-compose-samples"))
legacyDisableKotlinStrictApiMode = true
kotlinTarget = KotlinTarget.KOTLIN_1_9
}
diff --git a/navigation/navigation-compose/samples/build.gradle b/navigation/navigation-compose/samples/build.gradle
index b672a68..7cdaa05 100644
--- a/navigation/navigation-compose/samples/build.gradle
+++ b/navigation/navigation-compose/samples/build.gradle
@@ -42,7 +42,7 @@
implementation(project(":compose:animation:animation"))
implementation("androidx.compose.foundation:foundation:1.0.1")
implementation("androidx.compose.ui:ui-tooling:1.4.0")
- implementation(projectOrArtifact(":navigation:navigation-compose"))
+ implementation(project(":navigation:navigation-compose"))
implementation("androidx.compose.material:material:1.0.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.5.0")
@@ -57,10 +57,6 @@
kotlinTarget = KotlinTarget.KOTLIN_2_0
}
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
-}
-
android {
compileSdk 35
namespace "androidx.navigation.compose.samples"
diff --git a/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/SharedElementSample.kt b/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/SharedElementSample.kt
index 838929a6..bfb1c33 100644
--- a/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/SharedElementSample.kt
+++ b/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/SharedElementSample.kt
@@ -103,65 +103,77 @@
SharedTransitionLayout {
val selectFirst = mutableStateOf(true)
NavHost(navController, startDestination = RedBox) {
- composable<RedBox> { RedBox(this, selectFirst) { navController.navigate(BlueBox) } }
- composable<BlueBox> { BlueBox(this, selectFirst) { navController.popBackStack() } }
+ composable<RedBox> {
+ RedBox(this@SharedTransitionLayout, this, selectFirst) {
+ navController.navigate(BlueBox)
+ }
+ }
+ composable<BlueBox> {
+ BlueBox(this@SharedTransitionLayout, this, selectFirst) {
+ navController.popBackStack()
+ }
+ }
}
}
}
-context(SharedTransitionScope)
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun RedBox(
+ sharedScope: SharedTransitionScope,
scope: AnimatedContentScope,
selectFirst: MutableState<Boolean>,
onNavigate: () -> Unit
) {
- Box(
- Modifier.sharedBounds(
- rememberSharedContentState("name"),
- scope,
- renderInOverlayDuringTransition = selectFirst.value
- )
- .clickable(
- onClick = {
- selectFirst.value = !selectFirst.value
- onNavigate()
- }
- )
- .background(Color.Red)
- .size(100.dp)
- ) {
- Text("start", color = Color.White)
+ with(sharedScope) {
+ Box(
+ Modifier.sharedBounds(
+ rememberSharedContentState("name"),
+ scope,
+ renderInOverlayDuringTransition = selectFirst.value
+ )
+ .clickable(
+ onClick = {
+ selectFirst.value = !selectFirst.value
+ onNavigate()
+ }
+ )
+ .background(Color.Red)
+ .size(100.dp)
+ ) {
+ Text("start", color = Color.White)
+ }
}
}
-context(SharedTransitionScope)
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun BlueBox(
+ sharedScope: SharedTransitionScope,
scope: AnimatedContentScope,
selectFirst: MutableState<Boolean>,
onPopBack: () -> Unit
) {
- Box(
- Modifier.offset(180.dp, 180.dp)
- .sharedBounds(
- rememberSharedContentState("name"),
- scope,
- renderInOverlayDuringTransition = !selectFirst.value
- )
- .clickable(
- onClick = {
- selectFirst.value = !selectFirst.value
- onPopBack()
- }
- )
- .alpha(0.5f)
- .background(Color.Blue)
- .size(180.dp)
- ) {
- Text("finish", color = Color.White)
+ with(sharedScope) {
+ Box(
+ Modifier.offset(180.dp, 180.dp)
+ .sharedBounds(
+ rememberSharedContentState("name"),
+ scope,
+ renderInOverlayDuringTransition = !selectFirst.value
+ )
+ .clickable(
+ onClick = {
+ selectFirst.value = !selectFirst.value
+ onPopBack()
+ }
+ )
+ .alpha(0.5f)
+ .background(Color.Blue)
+ .size(180.dp)
+ ) {
+ Text("finish", color = Color.White)
+ }
}
}
diff --git a/navigation/navigation-dynamic-features-fragment/build.gradle b/navigation/navigation-dynamic-features-fragment/build.gradle
index 34a65f65..d294b3c 100644
--- a/navigation/navigation-dynamic-features-fragment/build.gradle
+++ b/navigation/navigation-dynamic-features-fragment/build.gradle
@@ -51,9 +51,9 @@
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.espressoCore)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
+ androidTestImplementation(libs.mockitoCore)
androidTestImplementation(libs.truth)
androidTestImplementation(project(":internal-testutils-runtime"), {
exclude group: "androidx.fragment", module: "fragment"
diff --git a/navigation/navigation-dynamic-features-runtime/build.gradle b/navigation/navigation-dynamic-features-runtime/build.gradle
index 8693621..ba25181 100644
--- a/navigation/navigation-dynamic-features-runtime/build.gradle
+++ b/navigation/navigation-dynamic-features-runtime/build.gradle
@@ -54,9 +54,9 @@
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.espressoCore)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
+ androidTestImplementation(libs.mockitoCore)
androidTestImplementation(libs.truth)
androidTestImplementation(project(":internal-testutils-runtime"), {
exclude group: "androidx.fragment", module: "fragment"
diff --git a/navigation/navigation-runtime-lint/build.gradle b/navigation/navigation-runtime-lint/build.gradle
index 91991d3..f50d052 100644
--- a/navigation/navigation-runtime-lint/build.gradle
+++ b/navigation/navigation-runtime-lint/build.gradle
@@ -35,7 +35,7 @@
dependencies {
compileOnly(libs.androidLintMinApi)
compileOnly(libs.kotlinStdlib)
- bundleInside(projectOrArtifact(":navigation:navigation-lint-common"))
+ bundleInside(project(":navigation:navigation-lint-common"))
testImplementation(libs.kotlinStdlib)
testImplementation(libs.androidLint)
diff --git a/navigation/navigation-runtime/build.gradle b/navigation/navigation-runtime/build.gradle
index 2698334..9acb9fd 100644
--- a/navigation/navigation-runtime/build.gradle
+++ b/navigation/navigation-runtime/build.gradle
@@ -42,7 +42,7 @@
implementation(libs.kotlinSerializationCore)
api(libs.kotlinStdlib)
- androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-runtime-testing"))
+ androidTestImplementation(project(":lifecycle:lifecycle-runtime-testing"))
androidTestImplementation(project(":internal-testutils-navigation"))
androidTestImplementation(project(":internal-testutils-runtime"))
androidTestImplementation(libs.testExtJunit)
@@ -53,8 +53,8 @@
androidTestImplementation(libs.espressoCore)
androidTestImplementation(libs.espressoIntents)
androidTestImplementation(libs.truth)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.kotlinStdlib)
androidTestImplementation(libs.kotlinTest)
diff --git a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt
index 986c95e..c05732dc 100644
--- a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt
+++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt
@@ -29,6 +29,7 @@
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.testing.TestLifecycleOwner
+import androidx.navigation.NavController.Companion.KEY_DEEP_LINK_INTENT
import androidx.navigation.NavDestination.Companion.createRoute
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.serialization.generateHashCode
@@ -814,6 +815,30 @@
@UiThreadTest
@Test
+ fun testNavigateContainsIntent() {
+ val navController = createNavController()
+ navController.graph =
+ navController.createGraph(startDestination = "start") {
+ test("start")
+ test("second")
+ }
+
+ val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+ assertThat(navController.currentDestination?.route).isEqualTo("start")
+ assertThat(navigator.backStack.size).isEqualTo(1)
+
+ navController.navigate("second")
+ assertThat(navController.currentDestination?.route).isEqualTo("second")
+ assertThat(navigator.backStack.size).isEqualTo(2)
+ val intent: Intent? =
+ @Suppress("DEPRECATION")
+ navController.currentBackStackEntry?.arguments?.getParcelable(KEY_DEEP_LINK_INTENT)
+ assertThat(intent).isNotNull()
+ assertThat(intent!!.data).isEqualTo(Uri.parse("android-app://androidx.navigation/second"))
+ }
+
+ @UiThreadTest
+ @Test
fun testNavigateNestedSharedDestination() {
val navController = createNavController()
navController.graph =
@@ -1524,7 +1549,6 @@
test<TestClass>()
}
assertThat(navController.currentDestination?.route).isEqualTo("start")
-
// passed in arg
navController.navigate(TestClass(TestTopLevelEnum.TWO))
assertThat(navController.currentDestination?.hasRoute(TestClass::class)).isTrue()
@@ -1725,12 +1749,10 @@
navController.graph = nav_singleArg_graph
// first nav with arg filed in
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13")
// second nav with arg filled in
- val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
- navController.navigate(deepLink2)
+ navController.navigate("second_test/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13", "second_test/18"]
@@ -1761,12 +1783,10 @@
}
// fist nested graph
- val deepLink = Uri.parse("android-app://androidx.navigation/graph2/13")
- navController.navigate(deepLink)
+ navController.navigate("graph2/13")
// second nested graph
- val deepLink2 = Uri.parse("android-app://androidx.navigation/graph3/18")
- navController.navigate(deepLink2)
+ navController.navigate("graph3/18")
val navigator = navController.navigatorProvider.getNavigator(NavGraphNavigator::class.java)
// ["graph", "graph2/13", "graph3/18"]
@@ -1786,8 +1806,7 @@
navController.graph = nav_multiArg_graph
// navigate with both args filed in
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13/18"]
@@ -1804,8 +1823,7 @@
navController.graph = nav_multiArg_graph
// navigate with args partially filed in
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/{arg2}")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13/{arg2}"]
@@ -1902,8 +1920,7 @@
}
// navigate with query param
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test?opt=13")
- navController.navigate(deepLink)
+ navController.navigate("second_test?opt=13")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -1937,8 +1954,7 @@
}
// navigate with query params
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test?opt=null&opt2=13")
- navController.navigate(deepLink)
+ navController.navigate("second_test?opt=null&opt2=13")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -1998,8 +2014,7 @@
navController.graph = nav_singleArg_graph
// navigate with arg filed in
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13"]
@@ -2024,8 +2039,7 @@
navController.graph = nav_multiArg_graph
// navigate with args partially filed in
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13/18"]
@@ -2049,8 +2063,7 @@
navController.graph = nav_multiArg_graph
// navigate with args partially filed in
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/{arg2}")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13/{arg2}"]
@@ -2073,8 +2086,7 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13/18"]
@@ -2446,12 +2458,10 @@
navController.graph = nav_singleArg_graph
// first nav with arg filed in
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13")
// second nav with arg filled in
- val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
- navController.navigate(deepLink2)
+ navController.navigate("second_test/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13", "second_test/18"]
@@ -2470,12 +2480,10 @@
navController.graph = nav_singleArg_graph
// first nav with arg filed in
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13")
// second nav with arg filled in
- val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
- navController.navigate(deepLink2)
+ navController.navigate("second_test/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13", "second_test/18"]
@@ -2492,8 +2500,7 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13/18"]
@@ -2510,8 +2517,7 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/{arg2}")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13/{arg2}"]
@@ -2539,8 +2545,7 @@
}
// navigate without filling query param
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test?{arg}")
- navController.navigate(deepLink)
+ navController.navigate("second_test?{arg}")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -2569,8 +2574,7 @@
}
// navigate without query param
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test?opt={arg}")
- navController.navigate(deepLink)
+ navController.navigate("second_test?opt={arg}")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -2596,8 +2600,7 @@
}
// navigate without query param
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test?opt=null")
- navController.navigate(deepLink)
+ navController.navigate("second_test?opt=null")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -2612,8 +2615,7 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13/18"]
@@ -2630,8 +2632,7 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/{arg2}")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13/{arg2}"]
@@ -2648,8 +2649,7 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13/18"]
@@ -2666,8 +2666,7 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13/18"]
@@ -2989,11 +2988,9 @@
val navController = createNavController()
navController.graph = nav_singleArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13")
- val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
- navController.navigate(deepLink2)
+ navController.navigate("second_test/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(3)
@@ -3012,11 +3009,9 @@
val navController = createNavController()
navController.graph = nav_singleArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13")
- val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
- navController.navigate(deepLink2)
+ navController.navigate("second_test/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(3)
@@ -3036,11 +3031,9 @@
val navController = createNavController()
navController.graph = nav_singleArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13")
- val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
- navController.navigate(deepLink2)
+ navController.navigate("second_test/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(3)
@@ -3090,11 +3083,9 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/14")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/14")
- val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18/19")
- navController.navigate(deepLink2)
+ navController.navigate("second_test/18/19")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(3)
@@ -3111,11 +3102,9 @@
val navController = createNavController()
navController.graph = nav_singleArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13")
- val deepLink3 = Uri.parse("android-app://androidx.navigation/second_test/18")
- navController.navigate(deepLink3)
+ navController.navigate("second_test/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(3)
@@ -3133,11 +3122,9 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/14")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/14")
- val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18/19")
- navController.navigate(deepLink2)
+ navController.navigate("second_test/18/19")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(3)
@@ -3155,11 +3142,9 @@
val navController = createNavController()
navController.graph = nav_singleArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13")
- val deepLink3 = Uri.parse("android-app://androidx.navigation/second_test/18")
- navController.navigate(deepLink3)
+ navController.navigate("second_test/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(3)
@@ -3179,8 +3164,7 @@
val navController = createNavController()
navController.graph = nav_singleArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -3201,8 +3185,7 @@
navController.graph = nav_multiArg_graph
// navigate with partial args filled in
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/{arg2}")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -3229,8 +3212,7 @@
}
// navigate without filling query param
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test?{arg}")
- navController.navigate(deepLink)
+ navController.navigate("second_test?{arg}")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -3258,8 +3240,7 @@
}
// navigate without filling query param
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test?opt={arg}")
- navController.navigate(deepLink)
+ navController.navigate("second_test?opt={arg}")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -3287,8 +3268,7 @@
}
// navigate without filling query param
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test?opt=null")
- navController.navigate(deepLink)
+ navController.navigate("second_test?opt=null")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -3305,8 +3285,7 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/{arg2}")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -3323,8 +3302,7 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -3341,8 +3319,7 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -3359,8 +3336,7 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -3846,6 +3822,71 @@
assertThat(navigator.backStack.size).isEqualTo(1)
}
+ @UiThreadTest
+ @Test
+ fun testNavigateViaUriOnlyIfDeepLinkExplicitlyAdded() {
+ val navController = createNavController()
+ navController.graph =
+ navController.createGraph(startDestination = "start") {
+ test("start") { deepLink { uriPattern = createRoute("explicit_start_deeplink") } }
+ }
+ val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+ assertThat(navController.currentDestination?.route).isEqualTo("start")
+ assertThat(navigator.backStack.size).isEqualTo(1)
+
+ val deepLink = Uri.parse(createRoute("explicit_start_deeplink"))
+
+ navController.navigate(deepLink)
+ assertThat(navController.currentDestination?.route).isEqualTo("start")
+ assertThat(navigator.backStack.size).isEqualTo(2)
+
+ // ensure can't deep link with destination's public route
+ val deepLink2 = Uri.parse(createRoute("start"))
+
+ val exception =
+ assertFailsWith<IllegalArgumentException> { navController.navigate(deepLink2) }
+ assertThat(exception.message)
+ .isEqualTo(
+ "Navigation destination that matches request " +
+ "NavDeepLinkRequest{ uri=android-app://androidx.navigation/start } " +
+ "cannot be found in the navigation graph NavGraph(0x0) " +
+ "startDestination={Destination(0xa2cd94f5) route=start}"
+ )
+ }
+
+ @UiThreadTest
+ @Test
+ fun testNavigateViaRequestOnlyIfDeepLinkExplicitlyAdded() {
+ val navController = createNavController()
+ navController.graph =
+ navController.createGraph(startDestination = "start") {
+ test("start") { deepLink { uriPattern = createRoute("explicit_start_deeplink") } }
+ }
+ val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+ assertThat(navController.currentDestination?.route).isEqualTo("start")
+ assertThat(navigator.backStack.size).isEqualTo(1)
+
+ val request =
+ NavDeepLinkRequest(Uri.parse(createRoute("explicit_start_deeplink")), null, null)
+
+ navController.navigate(request)
+ assertThat(navController.currentDestination?.route).isEqualTo("start")
+ assertThat(navigator.backStack.size).isEqualTo(2)
+
+ // ensure can't deep link with destination's public route
+ val request2 = NavDeepLinkRequest(Uri.parse(createRoute("start")), null, null)
+
+ val exception =
+ assertFailsWith<IllegalArgumentException> { navController.navigate(request2) }
+ assertThat(exception.message)
+ .isEqualTo(
+ "Navigation destination that matches request " +
+ "NavDeepLinkRequest{ uri=android-app://androidx.navigation/start } " +
+ "cannot be found in the navigation graph NavGraph(0x0) " +
+ "startDestination={Destination(0xa2cd94f5) route=start}"
+ )
+ }
+
@LargeTest
@Test
fun testNavigateViaImplicitDeepLink() {
@@ -5125,6 +5166,56 @@
@UiThreadTest
@Test
+ fun testHandleDeepLinkFromRouteOnlyIfExplicitlyAdded() {
+ val navController = createNavController()
+ navController.graph =
+ navController.createGraph(startDestination = "start") {
+ test("start") { deepLink { uriPattern = createRoute("explicit_start_deeplink") } }
+ }
+ val collectedDestinationRoutes = mutableListOf<String?>()
+ navController.addOnDestinationChangedListener { _, destination, _ ->
+ collectedDestinationRoutes.add(destination.route)
+ }
+
+ assertThat(collectedDestinationRoutes).containsExactly("start")
+
+ val intent =
+ Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse(createRoute("explicit_start_deeplink")),
+ ApplicationProvider.getApplicationContext() as Context,
+ TestActivity::class.java
+ )
+
+ assertWithMessage("handleDeepLink should return true with valid deep link")
+ .that(navController.handleDeepLink(intent))
+ .isTrue()
+
+ assertWithMessage("$collectedDestinationRoutes should have 2 destination id")
+ .that(collectedDestinationRoutes)
+ .hasSize(2)
+ assertThat(collectedDestinationRoutes).containsExactly("start", "start")
+
+ val intent2 =
+ Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse(createRoute("start")),
+ ApplicationProvider.getApplicationContext() as Context,
+ TestActivity::class.java
+ )
+
+ assertWithMessage("handleDeepLink should return false with invalid deep link")
+ .that(navController.handleDeepLink(intent2))
+ .isFalse()
+
+ assertWithMessage("$collectedDestinationRoutes should have 2 destination id")
+ .that(collectedDestinationRoutes)
+ .hasSize(2)
+ assertThat(collectedDestinationRoutes).containsExactly("start", "start")
+ }
+
+ @UiThreadTest
+ @Test
fun testHandleDeepLinkToRootInvalid() {
val navController = createNavController()
navController.graph = nav_simple_route_graph
diff --git a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt
index ae11abb..bcf2373 100644
--- a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt
+++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt
@@ -313,9 +313,9 @@
}
assertThat(expected.message)
.isEqualTo(
- "Deep link android-app://androidx.navigation/graph can't be used to open destination " +
- "NavGraph(0xa22391e1) startDestination=0x0.\n" +
- "Following required arguments are missing: [intArg]"
+ "Cannot set route \"graph\" for destination " +
+ "NavGraph(0x0) startDestination=0x0. Following required " +
+ "arguments are missing: [intArg]"
)
}
@@ -339,9 +339,9 @@
}
assertThat(expected.message)
.isEqualTo(
- "Deep link android-app://androidx.navigation/graph/{intArg} can't be used to " +
- "open destination NavGraph(0xf9423909) startDestination=0x0.\n" +
- "Following required arguments are missing: [longArg]"
+ "Cannot set route \"graph/{intArg}\" for destination " +
+ "NavGraph(0x0) startDestination=0x0. Following required " +
+ "arguments are missing: [longArg]"
)
}
@@ -365,9 +365,9 @@
}
assertThat(expected.message)
.isEqualTo(
- "Deep link android-app://androidx.navigation/graph can't be used to open " +
- "destination NavGraph(0xa22391e1) startDestination=0x0.\n" +
- "Following required arguments are missing: [intArg, longArg]"
+ "Cannot set route \"graph\" for destination NavGraph(0x0) " +
+ "startDestination=0x0. Following required arguments " +
+ "are missing: [intArg, longArg]"
)
}
@@ -389,9 +389,8 @@
}
assertThat(expected.message)
.isEqualTo(
- "Deep link android-app://androidx.navigation/dest1 can't be used to open " +
- "destination Destination(0xa1f3a662).\n" +
- "Following required arguments are missing: [intArg]"
+ "Cannot set route \"dest1\" for destination " +
+ "Destination(0x0). Following required arguments are missing: [intArg]"
)
}
@@ -420,9 +419,9 @@
}
assertThat(expected.message)
.isEqualTo(
- "Deep link android-app://androidx.navigation/dest1/{intArg} can't be used to " +
- "open destination Destination(0x994aa5a8).\n" +
- "Following required arguments are missing: [longArg]"
+ "Cannot set route \"dest1/{intArg}\" for " +
+ "destination Destination(0x0). Following required " +
+ "arguments are missing: [longArg]"
)
}
@@ -448,9 +447,7 @@
}
assertThat(expected.message)
.isEqualTo(
- "Deep link android-app://androidx.navigation/dest can't be used to open " +
- "destination Destination(0x78d64faf).\n" +
- "Following required arguments are missing: [intArg, longArg]"
+ "Cannot set route \"dest\" for destination Destination(0x0). Following required arguments are missing: [intArg, longArg]"
)
}
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
index 0075458..1d6d518 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
@@ -1683,7 +1683,7 @@
return null
}
// if not matched by routePattern, try matching with route args
- if (_graph!!.route == route || _graph!!.matchDeepLink(route) != null) {
+ if (_graph!!.route == route || _graph!!.matchRoute(route) != null) {
return _graph
}
return backQueue.getTopGraph().findNode(route)
@@ -2150,7 +2150,7 @@
backStackMap.values.removeAll { it == backStackId }
val backStackState = backStackStates.remove(backStackId)
- val matchingDeepLink = matchingDestination.matchDeepLink(route)
+ val matchingDeepLink = matchingDestination.matchRoute(route)
// check if the topmost NavBackStackEntryState contains the arguments in this
// matchingDeepLink. If not, we didn't find the correct stack.
val isCorrectStack =
@@ -2424,11 +2424,35 @@
navOptions: NavOptions? = null,
navigatorExtras: Navigator.Extras? = null
) {
- navigate(
- NavDeepLinkRequest.Builder.fromUri(createRoute(route).toUri()).build(),
- navOptions,
- navigatorExtras
- )
+ requireNotNull(_graph) {
+ "Cannot navigate to $route. Navigation graph has not been set for " +
+ "NavController $this."
+ }
+ val currGraph = backQueue.getTopGraph()
+ val deepLinkMatch =
+ currGraph.matchRouteComprehensive(
+ route,
+ searchChildren = true,
+ searchParent = true,
+ lastVisited = currGraph
+ )
+ if (deepLinkMatch != null) {
+ val destination = deepLinkMatch.destination
+ val args = destination.addInDefaultArgs(deepLinkMatch.matchingArgs) ?: Bundle()
+ val node = deepLinkMatch.destination
+ val intent =
+ Intent().apply {
+ setDataAndType(createRoute(destination.route).toUri(), null)
+ action = null
+ }
+ args.putParcelable(KEY_DEEP_LINK_INTENT, intent)
+ navigate(node, args, navOptions, navigatorExtras)
+ } else {
+ throw IllegalArgumentException(
+ "Navigation destination that matches route $route cannot be found in the " +
+ "navigation graph $_graph"
+ )
+ }
}
/**
@@ -2470,12 +2494,7 @@
navOptions: NavOptions? = null,
navigatorExtras: Navigator.Extras? = null
) {
- val finalRoute = generateRouteFilled(route)
- navigate(
- NavDeepLinkRequest.Builder.fromUri(createRoute(finalRoute).toUri()).build(),
- navOptions,
- navigatorExtras
- )
+ navigate(generateRouteFilled(route), navOptions, navigatorExtras)
}
/**
diff --git a/navigation/navigation-safe-args-generator/build.gradle b/navigation/navigation-safe-args-generator/build.gradle
index 6b926eb..8b2401e 100644
--- a/navigation/navigation-safe-args-generator/build.gradle
+++ b/navigation/navigation-safe-args-generator/build.gradle
@@ -49,7 +49,7 @@
testImplementation(libs.junit)
testImplementation(libs.googleCompileTesting)
- testImplementation(projectOrArtifact(":room:room-compiler-processing-testing"), {
+ testImplementation(project(":room:room-compiler-processing-testing"), {
exclude group: "androidx.room", module: "room-compiler-processing"
})
testImplementationAarAsJar(project(":navigation:navigation-common"))
diff --git a/navigation/navigation-testing/src/androidTest/java/androidx/navigation/testing/TestNavigatorStateTest.kt b/navigation/navigation-testing/src/androidTest/java/androidx/navigation/testing/TestNavigatorStateTest.kt
index 82e6e99..4d38bfa 100644
--- a/navigation/navigation-testing/src/androidTest/java/androidx/navigation/testing/TestNavigatorStateTest.kt
+++ b/navigation/navigation-testing/src/androidTest/java/androidx/navigation/testing/TestNavigatorStateTest.kt
@@ -27,6 +27,7 @@
import androidx.navigation.NavDestination
import androidx.navigation.NavOptions
import androidx.navigation.Navigator
+import androidx.navigation.SupportingPane
import androidx.navigation.navOptions
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -86,6 +87,26 @@
}
@Test
+ fun testSupportingPaneLifecycle() {
+ val navigator = SupportingPaneTestNavigator()
+ navigator.onAttach(state)
+ val firstEntry = state.createBackStackEntry(navigator.createDestination(), null)
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED)
+
+ navigator.navigate(listOf(firstEntry), null, null)
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+ val secondEntry = state.createBackStackEntry(navigator.createDestination(), null)
+ navigator.navigate(listOf(secondEntry), null, null)
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+ assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+ navigator.popBackStack(secondEntry, false)
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+ assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.DESTROYED)
+ }
+
+ @Test
fun testWithTransitionLifecycle() {
val navigator = TestTransitionNavigator()
navigator.onAttach(state)
@@ -129,6 +150,58 @@
}
@Test
+ fun testWithSupportingPaneTransitionLifecycle() {
+ val navigator = SupportingPaneTestTransitionNavigator()
+ navigator.onAttach(state)
+ val firstEntry = state.createBackStackEntry(navigator.createDestination(), null)
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED)
+
+ navigator.navigate(listOf(firstEntry), null, null)
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+
+ state.markTransitionComplete(firstEntry)
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+ val secondEntry = state.createBackStackEntry(navigator.createDestination(), null)
+ navigator.navigate(listOf(secondEntry), null, null)
+ // Both are started because they are SupportingPane destinations
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+ assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+ assertThat(state.transitionsInProgress.value.contains(firstEntry)).isTrue()
+
+ state.markTransitionComplete(secondEntry)
+ // Even though the secondEntry has completed its transition, the firstEntry
+ // hasn't completed its transition, so it shouldn't be resumed yet
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+ assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+ state.markTransitionComplete(firstEntry)
+ // Both are resumed because they are SupportingPane destinations that have finished
+ // their transitions
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+ assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+ navigator.popBackStack(secondEntry, true)
+ assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED)
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+
+ state.markTransitionComplete(firstEntry)
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+ state.markTransitionComplete(secondEntry)
+ assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.DESTROYED)
+
+ val restoredSecondEntry = state.restoreBackStackEntry(secondEntry)
+ navigator.navigate(listOf(restoredSecondEntry), null, null)
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+ assertThat(restoredSecondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+ assertThat(state.transitionsInProgress.value.contains(firstEntry)).isTrue()
+
+ state.markTransitionComplete(firstEntry)
+ state.markTransitionComplete(restoredSecondEntry)
+ assertThat(restoredSecondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+ }
+
+ @Test
fun testSameEntry() {
val navigator = TestTransitionNavigator()
navigator.onAttach(state)
@@ -362,6 +435,35 @@
internal class FloatingTestDestination(navigator: Navigator<out NavDestination>) :
NavDestination(navigator), FloatingWindow
+ @Navigator.Name("test")
+ internal class SupportingPaneTestNavigator : Navigator<SupportingPaneTestDestination>() {
+ override fun createDestination(): SupportingPaneTestDestination =
+ SupportingPaneTestDestination(this)
+ }
+
+ @Navigator.Name("test")
+ internal class SupportingPaneTestTransitionNavigator :
+ Navigator<SupportingPaneTestDestination>() {
+
+ override fun createDestination(): SupportingPaneTestDestination =
+ SupportingPaneTestDestination(this)
+
+ override fun navigate(
+ entries: List<NavBackStackEntry>,
+ navOptions: NavOptions?,
+ navigatorExtras: Extras?
+ ) {
+ entries.forEach { entry -> state.pushWithTransition(entry) }
+ }
+
+ override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) {
+ state.popWithTransition(popUpTo, savedState)
+ }
+ }
+
+ internal class SupportingPaneTestDestination(navigator: Navigator<out NavDestination>) :
+ NavDestination(navigator), SupportingPane
+
class TestViewModel : ViewModel() {
var wasCleared = false
diff --git a/navigation/navigation-testing/src/main/java/androidx/navigation/testing/TestNavigatorState.kt b/navigation/navigation-testing/src/main/java/androidx/navigation/testing/TestNavigatorState.kt
index beb1d0a..f26e922 100644
--- a/navigation/navigation-testing/src/main/java/androidx/navigation/testing/TestNavigatorState.kt
+++ b/navigation/navigation-testing/src/main/java/androidx/navigation/testing/TestNavigatorState.kt
@@ -25,6 +25,7 @@
import androidx.navigation.NavDestination
import androidx.navigation.NavViewModelStoreProvider
import androidx.navigation.NavigatorState
+import androidx.navigation.SupportingPane
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
@@ -174,6 +175,17 @@
} else {
Lifecycle.State.STARTED
}
+ previousEntry.destination is SupportingPane -> {
+ // Match the previous entry's destination, making sure
+ // a transitioning destination does not go to resumed
+ previousEntry.maxLifecycle.coerceAtMost(
+ if (!transitioning) {
+ Lifecycle.State.RESUMED
+ } else {
+ Lifecycle.State.STARTED
+ }
+ )
+ }
previousEntry.destination is FloatingWindow -> Lifecycle.State.STARTED
else -> Lifecycle.State.CREATED
}
diff --git a/paging/integration-tests/testapp/build.gradle b/paging/integration-tests/testapp/build.gradle
index a1918e9..891a0b3 100644
--- a/paging/integration-tests/testapp/build.gradle
+++ b/paging/integration-tests/testapp/build.gradle
@@ -24,17 +24,17 @@
dependencies {
implementation("androidx.arch.core:core-runtime:2.2.0")
- implementation(projectOrArtifact(":room:room-ktx"))
- implementation(projectOrArtifact(":room:room-rxjava2"))
- implementation(projectOrArtifact(":room:room-paging"))
+ implementation(project(":room:room-ktx"))
+ implementation(project(":room:room-rxjava2"))
+ implementation(project(":room:room-paging"))
implementation(project(":paging:paging-common-ktx"))
implementation(project(":paging:paging-runtime"))
implementation(project(":paging:paging-rxjava2"))
- ksp(projectOrArtifact(":room:room-compiler"))
+ ksp(project(":room:room-compiler"))
- implementation(projectOrArtifact(":recyclerview:recyclerview"))
+ implementation(project(":recyclerview:recyclerview"))
implementation("androidx.fragment:fragment-ktx:1.3.0")
implementation("androidx.appcompat:appcompat:1.1.0")
implementation(libs.kotlinStdlib)
diff --git a/paging/paging-compose/build.gradle b/paging/paging-compose/build.gradle
index 8d938df..df215df 100644
--- a/paging/paging-compose/build.gradle
+++ b/paging/paging-compose/build.gradle
@@ -52,16 +52,16 @@
commonTest {
dependencies {
- implementation projectOrArtifact(":compose:ui:ui-tooling")
+ implementation project(":compose:ui:ui-tooling")
implementation(project(":compose:test-utils"))
- implementation(projectOrArtifact(":internal-testutils-paging"))
+ implementation(project(":internal-testutils-paging"))
}
}
jvmTest {
dependsOn(commonTest)
dependencies {
- implementation(projectOrArtifact(":compose:ui:ui-test-junit4"))
+ implementation(project(":compose:ui:ui-test-junit4"))
}
}
diff --git a/paging/paging-compose/integration-tests/paging-demos/build.gradle b/paging/paging-compose/integration-tests/paging-demos/build.gradle
index 2541828..901505d 100644
--- a/paging/paging-compose/integration-tests/paging-demos/build.gradle
+++ b/paging/paging-compose/integration-tests/paging-demos/build.gradle
@@ -37,15 +37,15 @@
implementation("androidx.activity:activity:1.7.1")
implementation("androidx.lifecycle:lifecycle-common:2.6.1")
- implementation(projectOrArtifact(":compose:integration-tests:demos:common"))
- implementation(projectOrArtifact(":compose:foundation:foundation"))
- implementation(projectOrArtifact(":compose:material:material"))
- implementation(projectOrArtifact(":paging:paging-compose"))
- implementation(projectOrArtifact(":paging:paging-compose:paging-compose-samples"))
+ implementation(project(":compose:integration-tests:demos:common"))
+ implementation(project(":compose:foundation:foundation"))
+ implementation(project(":compose:material:material"))
+ implementation(project(":paging:paging-compose"))
+ implementation(project(":paging:paging-compose:paging-compose-samples"))
- ksp(projectOrArtifact(":room:room-compiler"))
- implementation(projectOrArtifact(":room:room-ktx"))
- implementation(projectOrArtifact(":room:room-paging"))
+ ksp(project(":room:room-compiler"))
+ implementation(project(":room:room-ktx"))
+ implementation(project(":room:room-paging"))
}
androidx {
diff --git a/paging/paging-runtime/build.gradle b/paging/paging-runtime/build.gradle
index e11cacf..c7ec26a 100644
--- a/paging/paging-runtime/build.gradle
+++ b/paging/paging-runtime/build.gradle
@@ -50,7 +50,7 @@
androidTestImplementation(project(":internal-testutils-ktx"))
androidTestImplementation(project(":internal-testutils-paging"))
androidTestImplementation(project(":paging:paging-testing"))
- androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-runtime-testing"))
+ androidTestImplementation(project(":lifecycle:lifecycle-runtime-testing"))
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testRunner)
diff --git a/pdf/integration-tests/testapp/build.gradle b/pdf/integration-tests/testapp/build.gradle
index 24a850b..66b4549 100644
--- a/pdf/integration-tests/testapp/build.gradle
+++ b/pdf/integration-tests/testapp/build.gradle
@@ -30,8 +30,8 @@
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.junit)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.truth)
androidTestImplementation(libs.espressoCore)
androidTestImplementation(project(":pdf:pdf-viewer-fragment"))
diff --git a/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt b/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
index d31fa8e..b8d85ac 100644
--- a/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
+++ b/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
@@ -18,11 +18,13 @@
import android.app.Activity
import android.content.ContentResolver
+import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.view.WindowManager
import android.widget.FrameLayout
import androidx.core.os.BundleCompat
import androidx.core.view.WindowInsetsCompat
@@ -66,6 +68,7 @@
import androidx.pdf.widget.ZoomView
import androidx.pdf.widget.ZoomView.ZoomScroll
import com.google.android.material.floatingactionbutton.FloatingActionButton
+import java.io.IOException
import kotlinx.coroutines.launch
/**
@@ -235,7 +238,6 @@
): View? {
super.onCreateView(inflater, container, savedInstanceState)
this.container = container
-
if (!hasContents && delayedContentsAvailable == null) {
if (savedInstanceState != null) {
restoreContents(savedInstanceState)
@@ -298,7 +300,7 @@
}
}
},
- onDocumentLoadFailure = { thrown -> onLoadDocumentError(thrown) }
+ onDocumentLoadFailure = { thrown -> showLoadingErrorView(thrown) }
)
setUpEditFab()
@@ -362,20 +364,25 @@
/** Adjusts the [FindInFileView] to be displayed on top of the keyboard. */
private fun adjustInsetsForSearchMenu(findInFileView: FindInFileView, activity: Activity) {
- val screenHeight = activity.resources.displayMetrics.heightPixels
+ val containerLocation = IntArray(2)
+ container!!.getLocationInWindow(containerLocation)
+
+ val windowManager = activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+ val screenHeight = windowManager.currentWindowMetrics.bounds.height()
+
val imeInsets =
activity.window.decorView.rootWindowInsets.getInsets(WindowInsetsCompat.Type.ime())
- var menuMargin = 0
val keyboardTop = screenHeight - imeInsets.bottom
- if (container!!.bottom >= keyboardTop) {
- menuMargin = container!!.bottom - keyboardTop
- }
+ val absoluteContainerBottom = container!!.height + containerLocation[1]
+ var menuMargin = 0
+ if (absoluteContainerBottom >= keyboardTop) {
+ menuMargin = absoluteContainerBottom - keyboardTop
+ }
findInFileView.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = menuMargin
}
-
isSearchMenuAdjusted = true
}
@@ -494,8 +501,11 @@
savedState?.let { state ->
if (isFileRestoring) {
state.containsKey(KEY_LAYOUT_REACH).let {
- val layoutReach = state.getInt(KEY_LAYOUT_REACH)
- layoutHandler?.setInitialPageLayoutReachWithMax(layoutReach)
+ val layoutReach = state.getInt(KEY_LAYOUT_REACH, -1)
+ if (layoutReach != -1) {
+ layoutHandler?.pageLayoutReach = layoutReach
+ layoutHandler?.setInitialPageLayoutReachWithMax(layoutReach)
+ }
}
// Restore page selection from saved state if it exists
@@ -529,12 +539,7 @@
paginatedView?.pageViewFactory = updatedPageViewFactory
selectionObserver =
- PageSelectionValueObserver(
- paginatedView!!,
- paginationModel!!,
- pageViewFactory!!,
- requireContext()
- )
+ PageSelectionValueObserver(paginatedView!!, pageViewFactory!!, requireContext())
pdfLoaderCallbacks?.selectionModel?.selection()?.addObserver(selectionObserver)
}
@@ -581,6 +586,7 @@
pdfLoaderCallbacks?.pdfLoader = pdfLoader
layoutHandler = LayoutHandler(pdfLoader)
+ paginatedView?.model?.size?.let { layoutHandler!!.pageLayoutReach = it }
val updatedSelectionModel = PdfSelectionModel(pdfLoader)
updateSelectionModel(updatedSelectionModel)
@@ -604,7 +610,6 @@
selectedMatchObserver =
SelectedMatchValueObserver(
paginatedView!!,
- paginationModel!!,
pageViewFactory!!,
zoomView!!,
layoutHandler!!,
@@ -630,7 +635,7 @@
// app that owns it has been killed by the system. We will still recover,
// but log this.
viewState.set(ViewState.ERROR)
- onLoadDocumentError(e)
+ showLoadingErrorView(e)
}
}
}
@@ -654,9 +659,7 @@
}
private fun destroyContentModel() {
-
pdfLoader?.cancelAll()
-
paginationModel = null
selectionHandles?.destroy()
@@ -737,6 +740,13 @@
)
}
+ private fun showLoadingErrorView(error: Throwable) {
+ context?.resources?.getString(R.string.error_cannot_open_pdf)?.let {
+ loadingView?.showErrorView(it)
+ }
+ onLoadDocumentError(error)
+ }
+
private fun loadFile(fileUri: Uri) {
Preconditions.checkNotNull(fileUri)
Preconditions.checkArgument(
@@ -759,8 +769,13 @@
try {
validateFileUri(fileUri)
fetchFile(fileUri)
- } catch (e: SecurityException) {
- onLoadDocumentError(e)
+ } catch (error: Exception) {
+ when (error) {
+ is IOException,
+ is SecurityException,
+ is NullPointerException -> showLoadingErrorView(error)
+ else -> throw error
+ }
}
if (localUri != null && localUri != fileUri) {
annotationButton?.hide()
@@ -787,7 +802,7 @@
}
override fun failed(thrown: Throwable) {
- onLoadDocumentError(thrown)
+ showLoadingErrorView(thrown)
}
override fun progress(progress: Float) {}
diff --git a/pdf/pdf-viewer/build.gradle b/pdf/pdf-viewer/build.gradle
index 78bb274..595a07f 100644
--- a/pdf/pdf-viewer/build.gradle
+++ b/pdf/pdf-viewer/build.gradle
@@ -51,8 +51,8 @@
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.junit)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.truth)
androidTestImplementation(libs.espressoCore)
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/find/FindInFileView.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/find/FindInFileView.java
index 46be5f5..bd32145 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/find/FindInFileView.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/find/FindInFileView.java
@@ -17,6 +17,8 @@
package androidx.pdf.find;
import android.content.Context;
+import android.os.Bundle;
+import android.os.Parcelable;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
@@ -34,16 +36,19 @@
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.pdf.R;
+import androidx.pdf.models.MatchRects;
import androidx.pdf.util.Accessibility;
import androidx.pdf.util.CycleRange;
import androidx.pdf.util.ObservableValue;
import androidx.pdf.util.ObservableValue.ValueObserver;
import androidx.pdf.viewer.PaginatedView;
import androidx.pdf.viewer.SearchModel;
+import androidx.pdf.viewer.SelectedMatch;
import androidx.pdf.viewer.loader.PdfLoader;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import java.util.Objects;
/**
* A View that has a search query box, find-next and find-previous button, useful for finding
@@ -52,6 +57,12 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class FindInFileView extends LinearLayout {
+ private static final char MATCH_STATUS_COUNTING = '\u2026';
+ private static final String KEY_SUPER = "super";
+ private static final String KEY_IS_SAVED = "is_saved";
+ private static final String KEY_MATCH_RECTS = "match_rects";
+ private static final String KEY_SELECTED_PAGE = "selected_page";
+ private static final String KEY_SELECTED_INDEX = "selected_index";
private TextView mQueryBox;
private ImageView mPrevButton;
@@ -59,14 +70,20 @@
private TextView mMatchStatus;
private View mCloseButton;
private FloatingActionButton mAnnotationButton;
- private FindInFileListener mFindInFileListener;
- private ObservableValue<MatchCount> mMatchCount;
- private SearchModel mSearchModel;
private PaginatedView mPaginatedView;
+
+ private FindInFileListener mFindInFileListener;
private Runnable mOnClosedButtonCallback;
+
+ private SearchModel mSearchModel;
+ private ObservableValue<MatchCount> mMatchCount;
+
private boolean mIsAnnotationIntentResolvable;
- private static final char MATCH_STATUS_COUNTING = '\u2026';
- private static final String TAG = FindInFileView.class.getSimpleName();
+ private boolean mIsRestoring;
+ private int mViewingPage;
+ private int mSelectedMatch;
+ private MatchRects mMatches;
+
private final OnClickListener mOnClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
@@ -164,6 +181,34 @@
this.setFocusableInTouchMode(true);
}
+ @NonNull
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(KEY_SUPER, super.onSaveInstanceState());
+ if (mSearchModel != null && mSearchModel.selectedMatch().get() != null) {
+ bundle.putBoolean(KEY_IS_SAVED, true);
+ bundle.putParcelable(KEY_MATCH_RECTS, Objects.requireNonNull(
+ mSearchModel.selectedMatch().get()).getPageMatches());
+ bundle.putInt(KEY_SELECTED_PAGE, mSearchModel.getSelectedPage());
+ bundle.putInt(KEY_SELECTED_INDEX,
+ Objects.requireNonNull(mSearchModel.selectedMatch().get()).getSelected());
+ }
+ return bundle;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ Bundle bundle = (Bundle) state;
+ super.onRestoreInstanceState(bundle.getParcelable(KEY_SUPER, Parcelable.class));
+ if (bundle.getBoolean(KEY_IS_SAVED)) {
+ mIsRestoring = true;
+ mSelectedMatch = bundle.getInt(KEY_SELECTED_INDEX);
+ mViewingPage = bundle.getInt(KEY_SELECTED_PAGE);
+ mMatches = bundle.getParcelable(KEY_MATCH_RECTS, MatchRects.class);
+ }
+ }
+
/**
* Sets the pdfLoader and create a new {@link SearchModel} instance with the given pdfLoader.
*/
@@ -209,6 +254,9 @@
mAnnotationButton.hide();
}
setupFindInFileBtn();
+ if (mIsRestoring) {
+ restoreSelectedMatch();
+ }
} else {
this.setVisibility(GONE);
}
@@ -220,6 +268,17 @@
this.setVisibility(GONE);
mQueryBox.clearFocus();
mQueryBox.setText("");
+ mIsRestoring = false;
+ }
+
+ private void restoreSelectedMatch() {
+ // If the first match is selected, no need to restore since it will be reselected by default
+ if (mSelectedMatch > 0) {
+ mSearchModel.setSelectedMatch(
+ new SelectedMatch(mSearchModel.query().get(), mViewingPage, mMatches,
+ mSelectedMatch - 1));
+ mSearchModel.selectNextMatch(CycleRange.Direction.FORWARDS, mViewingPage);
+ }
}
private void setupFindInFileBtn() {
@@ -227,11 +286,7 @@
queryBoxRequestFocus();
mCloseButton.setOnClickListener(view -> {
- mOnClosedButtonCallback.run();
- View parentLayout = (View) mCloseButton.getParent();
- mQueryBox.clearFocus();
- mQueryBox.setText("");
- parentLayout.setVisibility(GONE);
+ resetFindInFile();
if (mIsAnnotationIntentResolvable) {
mAnnotationButton.show();
}
@@ -243,8 +298,7 @@
@Override
public boolean onQueryTextChange(@androidx.annotation.Nullable String query) {
if (mSearchModel != null && mPaginatedView != null) {
- mSearchModel.setQuery(query,
- mPaginatedView.getPageRangeHandler().getVisiblePage());
+ mSearchModel.setQuery(query, getViewingPage());
return true;
}
return false;
@@ -305,4 +359,11 @@
private void queryBoxRequestFocus() {
mQueryBox.requestFocus();
}
+
+ private int getViewingPage() {
+ if (mIsRestoring) {
+ return mViewingPage;
+ }
+ return mPaginatedView.getPageRangeHandler().getVisiblePage();
+ }
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageSelectionValueObserver.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageSelectionValueObserver.java
index 9314bd2..ddcc5bb 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageSelectionValueObserver.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageSelectionValueObserver.java
@@ -29,15 +29,12 @@
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class PageSelectionValueObserver implements ObservableValue.ValueObserver<PageSelection> {
private final PaginatedView mPaginatedView;
- private final PaginationModel mPaginationModel;
private final PageViewFactory mPageViewFactory;
private Context mContext;
public PageSelectionValueObserver(@NonNull PaginatedView paginatedView,
- @NonNull PaginationModel paginationModel,
@NonNull PageViewFactory pageViewFactory, @NonNull Context context) {
mPaginatedView = paginatedView;
- mPaginationModel = paginationModel;
mPageViewFactory = pageViewFactory;
mContext = context;
}
@@ -56,13 +53,14 @@
mPageViewFactory.getOrCreatePageView(
newSelection.getPage(),
PaginationUtils.getPageElevationInPixels(mContext),
- mPaginationModel.getPageSize(newSelection.getPage()))
+ mPaginatedView.getModel().getPageSize(newSelection.getPage()))
.setOverlay(new PdfHighlightOverlay(newSelection));
}
}
private boolean isPageCreated(int pageNum) {
- return pageNum < mPaginationModel.getSize() && mPaginatedView.getViewAt(pageNum) != null;
+ return pageNum < mPaginatedView.getModel().getSize() && mPaginatedView.getViewAt(pageNum)
+ != null;
}
private PageViewFactory.PageView getPage(int pageNum) {
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java
index 3cdd35d..ea847f0 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java
@@ -18,6 +18,8 @@
import android.content.Context;
import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.View;
@@ -27,6 +29,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
+import androidx.core.os.ParcelCompat;
import androidx.pdf.ViewState;
import androidx.pdf.data.Range;
import androidx.pdf.util.PaginationUtils;
@@ -168,6 +171,22 @@
}
}
+ @Nullable
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+ return new SavedState(superState, mModel);
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ SavedState savedState = (SavedState) state;
+ super.onRestoreInstanceState(((SavedState) state).getSuperState());
+ mModel = savedState.mModel;
+ mPageRangeHandler = new PageRangeHandler(mModel);
+ requestLayout();
+ }
+
/**
* Returns the current viewport in content coordinates
*/
@@ -522,4 +541,24 @@
public boolean isConfigurationChanged() {
return mIsConfigurationChanged;
}
+
+ static class SavedState extends View.BaseSavedState {
+ final PaginationModel mModel;
+
+ SavedState(Parcelable superState, PaginationModel model) {
+ super(superState);
+ mModel = model;
+ }
+
+ SavedState(Parcel source, ClassLoader loader) {
+ super(source);
+ mModel = ParcelCompat.readParcelable(source, loader, PaginationModel.class);
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeParcelable(mModel, flags);
+ }
+ }
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModel.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModel.java
index 8407a743..3279dfe 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModel.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModel.java
@@ -16,8 +16,11 @@
package androidx.pdf.viewer;
+import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
@@ -57,7 +60,8 @@
* pages are added
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class PaginationModel {
+@SuppressLint("BanParcelableUsage")
+public class PaginationModel implements Parcelable {
/**
* The spacing added before and after each page (the actual space between 2 consecutive pages is
* twice this distance), in pixels.
@@ -87,6 +91,28 @@
mPageSpacingPx = PaginationUtils.getPageSpacingInPixels(context);
}
+ protected PaginationModel(@NonNull Parcel in) {
+ mPageSpacingPx = in.readInt();
+ mMaxPages = in.readInt();
+ mPages = in.createTypedArray(Dimensions.CREATOR);
+ mPageStops = in.createIntArray();
+ mSize = in.readInt();
+ mEstimatedPageHeight = in.readFloat();
+ mAccumulatedPageSize = in.readInt();
+ }
+
+ public static final Creator<PaginationModel> CREATOR = new Creator<PaginationModel>() {
+ @Override
+ public PaginationModel createFromParcel(Parcel in) {
+ return new PaginationModel(in);
+ }
+
+ @Override
+ public PaginationModel[] newArray(int size) {
+ return new PaginationModel[size];
+ }
+ };
+
/**
* Initializes the model.
*
@@ -264,7 +290,6 @@
}
-
/**
* Returns the location of the page in the model.
*
@@ -277,7 +302,7 @@
* maximizes the portion of that view that is visible on the screen
* </ul>
*
- * @param pageNum - index of requested page
+ * @param pageNum - index of requested page
* @param viewArea - the current viewport in content coordinates
* @return - coordinates of the page within this model
*/
@@ -391,4 +416,20 @@
mObservers.clear();
super.finalize();
}
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mPageSpacingPx);
+ dest.writeInt(mMaxPages);
+ dest.writeTypedArray(mPages, flags);
+ dest.writeIntArray(mPageStops);
+ dest.writeInt(mSize);
+ dest.writeFloat(mEstimatedPageHeight);
+ dest.writeInt(mAccumulatedPageSize);
+ }
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfViewer.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfViewer.java
index 02df1c8..c7ff01b 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfViewer.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfViewer.java
@@ -286,12 +286,12 @@
mPaginatedView.setPageViewFactory(mPageViewFactory);
mSelectionObserver =
- new PageSelectionValueObserver(mPaginatedView, mPaginationModel, mPageViewFactory,
+ new PageSelectionValueObserver(mPaginatedView, mPageViewFactory,
requireContext());
mSelectionModel.selection().addObserver(mSelectionObserver);
mSelectedMatchObserver =
- new SelectedMatchValueObserver(mPaginatedView, mPaginationModel, mPageViewFactory,
+ new SelectedMatchValueObserver(mPaginatedView, mPageViewFactory,
mZoomView, mLayoutHandler, requireContext());
mSearchModel.selectedMatch().addObserver(mSelectedMatchObserver);
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SearchModel.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SearchModel.java
index 626930d..09438bf 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SearchModel.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SearchModel.java
@@ -352,4 +352,9 @@
public static String whiteSpaceToNull(@NonNull String query) {
return (query != null && TextUtils.isGraphic(query)) ? query : null;
}
+
+ /** Update the current selected match */
+ public void setSelectedMatch(@NonNull SelectedMatch selectedMatch) {
+ mSelectedMatch.set(selectedMatch);
+ }
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SelectedMatch.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SelectedMatch.java
index 9979a5d..9da8f85 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SelectedMatch.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SelectedMatch.java
@@ -46,7 +46,8 @@
* the
* matches is selected, or one with no matches.
*/
- SelectedMatch(String query, int page, MatchRects pageMatches, int selected) {
+ public SelectedMatch(@Nullable String query, int page, @Nullable MatchRects pageMatches,
+ int selected) {
Preconditions.checkNotNull(query);
Preconditions.checkNotNull(pageMatches);
Preconditions.checkArgument(!pageMatches.isEmpty(), "Cannot select empty matches");
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SelectedMatchValueObserver.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SelectedMatchValueObserver.java
index 55a9e21..1acf4b2 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SelectedMatchValueObserver.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SelectedMatchValueObserver.java
@@ -29,18 +29,16 @@
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class SelectedMatchValueObserver implements ObservableValue.ValueObserver<SelectedMatch> {
private final PaginatedView mPaginatedView;
- private final PaginationModel mPaginationModel;
private final PageViewFactory mPageViewFactory;
private final LayoutHandler mLayoutHandler;
private final ZoomView mZoomView;
private final Context mContext;
public SelectedMatchValueObserver(@NonNull PaginatedView paginatedView,
- @NonNull PaginationModel paginationModel, @NonNull PageViewFactory pageViewFactory,
+ @NonNull PageViewFactory pageViewFactory,
@NonNull ZoomView zoomView, @NonNull LayoutHandler layoutHandler,
@NonNull Context context) {
mPaginatedView = paginatedView;
- mPaginationModel = paginationModel;
mPageViewFactory = pageViewFactory;
mZoomView = zoomView;
mLayoutHandler = layoutHandler;
@@ -67,26 +65,27 @@
}
private boolean isPageCreated(int pageNum) {
- return pageNum < mPaginationModel.getSize() && mPaginatedView.getViewAt(pageNum) != null;
+ return pageNum < mPaginatedView.getModel().getSize() && mPaginatedView.getViewAt(pageNum)
+ != null;
}
private void lookAtSelection(SelectedMatch selection) {
if (selection == null || selection.isEmpty()) {
return;
}
- if (selection.getPage() >= mPaginationModel.getSize()) {
+ if (selection.getPage() >= mPaginatedView.getModel().getSize()) {
mLayoutHandler.layoutPages(selection.getPage() + 1);
return;
}
Rect rect = selection.getPageMatches().getFirstRect(selection.getSelected());
- int x = mPaginationModel.getLookAtX(selection.getPage(), rect.centerX());
- int y = mPaginationModel.getLookAtY(selection.getPage(), rect.centerY());
+ int x = mPaginatedView.getModel().getLookAtX(selection.getPage(), rect.centerX());
+ int y = mPaginatedView.getModel().getLookAtY(selection.getPage(), rect.centerY());
mZoomView.centerAt(x, y);
PageMosaicView pageView = mPageViewFactory.getOrCreatePageView(
selection.getPage(),
PaginationUtils.getPageElevationInPixels(mContext),
- mPaginationModel.getPageSize(selection.getPage()));
+ mPaginatedView.getModel().getPageSize(selection.getPage()));
pageView.setOverlay(selection.getOverlay());
}
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacksImpl.kt b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacksImpl.kt
index b5fbe27..820d9d5 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacksImpl.kt
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacksImpl.kt
@@ -210,10 +210,7 @@
searchModel?.setNumPages(numPages)
}
- if (isTextSearchActive) {
- findInFileView.setFindInFileView(true)
- findInFileView.setVisibility(VISIBLE)
- }
+ findInFileView.setFindInFileView(isTextSearchActive)
}
override fun documentNotLoaded(status: PdfStatus) {
@@ -228,9 +225,6 @@
"Document not loaded but status " + status.number
)
PdfStatus.PDF_ERROR -> {
- loadingView.showErrorView(
- context.resources.getString(R.string.error_cannot_open_pdf)
- )
handleError(status)
}
PdfStatus.FILE_ERROR,
@@ -259,9 +253,7 @@
override fun setPageDimensions(pageNum: Int, dimensions: Dimensions) {
if (viewState.get() != ViewState.NO_VIEW) {
-
paginatedView.model.addPage(pageNum, dimensions)
-
layoutHandler!!.pageLayoutReach = paginatedView.model.size
if (
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/FastScrollView.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/FastScrollView.java
index c34e569..1fbd636 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/FastScrollView.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/FastScrollView.java
@@ -150,6 +150,7 @@
super.onViewRemoved(child);
// Prevent leaks if ZoomView is removed from this ViewGroup.
if (child instanceof ZoomView && child == mZoomView) {
+ mZoomView.zoomScroll().removeObserver(mZoomScrollObserver);
mZoomView = null;
}
}
@@ -165,9 +166,7 @@
mZoomViewBasePadding =
new Rect(
mZoomView.getPaddingLeft(),
- mZoomView.getPaddingTop()
- + getResources().getDimensionPixelSize(
- R.dimen.viewer_doc_additional_top_offset),
+ mZoomView.getPaddingTop(),
mZoomView.getPaddingRight(),
mZoomView.getPaddingBottom());
mZoomViewConfigured = true;
@@ -183,9 +182,9 @@
if (mZoomView != null) {
mZoomView.setPadding(
0,
- mZoomViewBasePadding.top + insetsCompat.top,
+ mZoomViewBasePadding.top,
0,
- mZoomViewBasePadding.bottom + insetsCompat.bottom);
+ mZoomViewBasePadding.bottom);
setScrollbarMarginTop(mZoomView.getPaddingTop());
// Ignore ZoomView's intrinsic padding on the right side as we want it to be
// right-anchored
@@ -200,7 +199,6 @@
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mZoomView != null && mZoomViewConfigured) {
- mZoomView.zoomScroll().removeObserver(mZoomScrollObserver);
mZoomViewConfigured = false;
}
if (mPaginationModel != null) {
diff --git a/pdf/pdf-viewer/src/main/res/values-af/strings.xml b/pdf/pdf-viewer/src/main/res/values-af/strings.xml
index 3efa5d2..a35820c 100644
--- a/pdf/pdf-viewer/src/main/res/values-af/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-af/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Kon nie die lêer oopmaak nie. Moontlike toestemmingkwessie?"</string>
<string name="page_broken" msgid="2968770793669433462">"Bladsy is vir die PDF-dokument gebreek"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Onvoldoende data om die PDF-dokument te verwerk"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Kan nie PDF-lêer oopmaak nie"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-am/strings.xml b/pdf/pdf-viewer/src/main/res/values-am/strings.xml
index a2679692..02183fd 100644
--- a/pdf/pdf-viewer/src/main/res/values-am/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-am/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"ፋይሉን መክፈት አልተሳካም። የፈቃድ ችግር ሊሆን ይችላል?"</string>
<string name="page_broken" msgid="2968770793669433462">"ለPDF ሰነዱ ገፅ ተበላሽቷል"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF ሰነዱን ለማሰናዳት በቂ ያልሆነ ውሂብ"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF ፋይል መክፈት አይቻልም"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-ar/strings.xml b/pdf/pdf-viewer/src/main/res/values-ar/strings.xml
index eae5948..0bddaff 100644
--- a/pdf/pdf-viewer/src/main/res/values-ar/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-ar/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"تعذّر فتح الملف. هل توجد مشكلة محتملة في الأذونات؟"</string>
<string name="page_broken" msgid="2968770793669433462">"تعذّر تحميل صفحة من مستند PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"البيانات غير كافية لمعالجة مستند PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"يتعذّر فتح ملف PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-as/strings.xml b/pdf/pdf-viewer/src/main/res/values-as/strings.xml
index f9c9ef7..c165c74 100644
--- a/pdf/pdf-viewer/src/main/res/values-as/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-as/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"ফাইলটো খুলিব পৰা নগ’ল। সম্ভাব্য অনুমতি সম্পৰ্কীয় সমস্যা?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF নথিৰ বাবে পৃষ্ঠাখন বিসংগতিপূৰ্ণ"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF নথিখন প্ৰক্ৰিয়াকৰণ কৰিবলৈ অপৰ্যাপ্ত ডেটা"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF ফাইল খুলিব নোৱাৰি"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-az/strings.xml b/pdf/pdf-viewer/src/main/res/values-az/strings.xml
index 6ef1751..3222a01 100644
--- a/pdf/pdf-viewer/src/main/res/values-az/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-az/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Fayl açılmadı. İcazə problemi var?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF sənədi üçün səhifədə xəta var"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF sənədini emal etmək üçün kifayət qədər data yoxdur"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF faylını açmaq olmur"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-b+sr+Latn/strings.xml b/pdf/pdf-viewer/src/main/res/values-b+sr+Latn/strings.xml
index f259654..e6cbe55 100644
--- a/pdf/pdf-viewer/src/main/res/values-b+sr+Latn/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-b+sr+Latn/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Otvaranje fajla nije uspelo. Možda postoje problemi sa dozvolom?"</string>
<string name="page_broken" msgid="2968770793669433462">"Neispravna stranica za PDF dokument"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Nedovoljno podataka za obradu PDF dokumenta"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Otvaranje PDF fajla nije uspelo"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-be/strings.xml b/pdf/pdf-viewer/src/main/res/values-be/strings.xml
index c951b2c..0ca6aa3 100644
--- a/pdf/pdf-viewer/src/main/res/values-be/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-be/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Не ўдалося адкрыць файл. Магчыма, праблема з дазволам?"</string>
<string name="page_broken" msgid="2968770793669433462">"Старонка дакумента PDF пашкоджана"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Не хапае даных для апрацоўкі дакумента PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Не ўдаецца адкрыць файл PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-bn/strings.xml b/pdf/pdf-viewer/src/main/res/values-bn/strings.xml
index 937be09..063be07 100644
--- a/pdf/pdf-viewer/src/main/res/values-bn/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-bn/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"ফাইল খোলা যায়নি। অনুমতি সংক্রান্ত সমস্যার কারণে এটি হতে পারে?"</string>
<string name="page_broken" msgid="2968770793669433462">"পিডিএফ ডকুমেন্টের ক্ষেত্রে পৃষ্ঠা ভেঙে গেছে"</string>
<string name="needs_more_data" msgid="3520133467908240802">"পিডিএফ ডকুমেন্ট প্রসেস করার জন্য যথেষ্ট ডেটা নেই"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF ফাইল খোলা যাচ্ছে না"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-bs/strings.xml b/pdf/pdf-viewer/src/main/res/values-bs/strings.xml
index cd49b7a..9d4f998 100644
--- a/pdf/pdf-viewer/src/main/res/values-bs/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-bs/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Otvaranje fajla nije uspjelo. Možda postoji problem s odobrenjem?"</string>
<string name="page_broken" msgid="2968770793669433462">"Stranica je prelomljena za PDF dokument"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Nema dovoljno podataka za obradu PDF dokumenta"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Nije moguće otvoriti PDF fajl"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-cs/strings.xml b/pdf/pdf-viewer/src/main/res/values-cs/strings.xml
index cdc4b65..259ed4a 100644
--- a/pdf/pdf-viewer/src/main/res/values-cs/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-cs/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Soubor se nepodařilo otevřít. Může se jednat o problém s oprávněním."</string>
<string name="page_broken" msgid="2968770793669433462">"Dokument PDF obsahuje poškozenou stránku"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Nedostatek dat ke zpracování dokumentu PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Soubor PDF se nepodařilo otevřít"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-da/strings.xml b/pdf/pdf-viewer/src/main/res/values-da/strings.xml
index 5bcd02c..d132b90e 100644
--- a/pdf/pdf-viewer/src/main/res/values-da/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-da/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Filen kunne ikke åbnes Mon der er et problem med tilladelserne?"</string>
<string name="page_broken" msgid="2968770793669433462">"Siden er ødelagt for PDF-dokumentet"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Der er ikke nok data til at behandle PDF-dokumentet"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF-filen kan ikke åbnes"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-de/strings.xml b/pdf/pdf-viewer/src/main/res/values-de/strings.xml
index 72b1096..7e7ed4c 100644
--- a/pdf/pdf-viewer/src/main/res/values-de/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-de/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Datei konnte nicht geöffnet werden. Möglicherweise ein Berechtigungsproblem?"</string>
<string name="page_broken" msgid="2968770793669433462">"Seite für PDF-Dokument ist fehlerhaft"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Keine ausreichenden Daten, um das PDF-Dokument zu verarbeiten"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF‑Datei kann nicht geöffnet werden"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-el/strings.xml b/pdf/pdf-viewer/src/main/res/values-el/strings.xml
index ada33d8..95d082c 100644
--- a/pdf/pdf-viewer/src/main/res/values-el/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-el/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Δεν ήταν δυνατό το άνοιγμα του αρχείου. Μήπως υπάρχει κάποιο πρόβλημα με την άδεια;"</string>
<string name="page_broken" msgid="2968770793669433462">"Δεν ήταν δυνατή η φόρτωση του εγγράφου PDF από τη σελίδα"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Μη επαρκή δεδομένα για την επεξεργασία του εγγράφου PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Δεν είναι δυνατό το άνοιγμα του αρχείου PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-en-rAU/strings.xml b/pdf/pdf-viewer/src/main/res/values-en-rAU/strings.xml
index 2f9e726..41add4b 100644
--- a/pdf/pdf-viewer/src/main/res/values-en-rAU/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-en-rAU/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Failed to open the file. Possible permission issue?"</string>
<string name="page_broken" msgid="2968770793669433462">"Page broken for the PDF document"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Insufficient data for processing the PDF document"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Can\'t open PDF file"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-en-rGB/strings.xml b/pdf/pdf-viewer/src/main/res/values-en-rGB/strings.xml
index 2f9e726..41add4b 100644
--- a/pdf/pdf-viewer/src/main/res/values-en-rGB/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-en-rGB/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Failed to open the file. Possible permission issue?"</string>
<string name="page_broken" msgid="2968770793669433462">"Page broken for the PDF document"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Insufficient data for processing the PDF document"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Can\'t open PDF file"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-en-rIN/strings.xml b/pdf/pdf-viewer/src/main/res/values-en-rIN/strings.xml
index 2f9e726..41add4b 100644
--- a/pdf/pdf-viewer/src/main/res/values-en-rIN/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-en-rIN/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Failed to open the file. Possible permission issue?"</string>
<string name="page_broken" msgid="2968770793669433462">"Page broken for the PDF document"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Insufficient data for processing the PDF document"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Can\'t open PDF file"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-es-rUS/strings.xml b/pdf/pdf-viewer/src/main/res/values-es-rUS/strings.xml
index 388c071..eadfc4b 100644
--- a/pdf/pdf-viewer/src/main/res/values-es-rUS/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-es-rUS/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"No se pudo abrir el archivo. ¿Puede que se deba a un problema de permisos?"</string>
<string name="page_broken" msgid="2968770793669433462">"La página no funciona para el documento PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"No hay datos suficientes para procesar el documento PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"No se puede abrir el archivo PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-es/strings.xml b/pdf/pdf-viewer/src/main/res/values-es/strings.xml
index b1aca5f..6fdeb58 100644
--- a/pdf/pdf-viewer/src/main/res/values-es/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-es/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"No se ha podido abrir el archivo. ¿Puede que se deba a un problema de permisos?"</string>
<string name="page_broken" msgid="2968770793669433462">"La página no funciona para el documento PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Datos insuficientes para procesar el documento PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"No se puede abrir el archivo PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-et/strings.xml b/pdf/pdf-viewer/src/main/res/values-et/strings.xml
index d640ef5..91eb54d3 100644
--- a/pdf/pdf-viewer/src/main/res/values-et/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-et/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Faili avamine nurjus. Probleem võib olla seotud lubadega."</string>
<string name="page_broken" msgid="2968770793669433462">"Rikutud leht PDF-dokumendis"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF-dokumendi töötlemiseks pole piisavalt andmeid"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF-faili ei saa avada"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-eu/strings.xml b/pdf/pdf-viewer/src/main/res/values-eu/strings.xml
index 20770aa..ba3a027 100644
--- a/pdf/pdf-viewer/src/main/res/values-eu/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-eu/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Ezin izan da ireki fitxategia. Agian ez duzu horretarako baimenik?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF dokumentuaren orria hondatuta dago"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Ez dago behar adina daturik PDF dokumentua prozesatzeko"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Ezin da ireki PDF fitxategia"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-fa/strings.xml b/pdf/pdf-viewer/src/main/res/values-fa/strings.xml
index 0669b17..30063e7 100644
--- a/pdf/pdf-viewer/src/main/res/values-fa/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-fa/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"فایل باز نشد. احتمالاً مشکلی در اجازه وجود دارد؟"</string>
<string name="page_broken" msgid="2968770793669433462">"صفحه سند PDF خراب است"</string>
<string name="needs_more_data" msgid="3520133467908240802">"دادهها برای پردازش سند PDF کافی نیست"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"فایل PDF باز نشد"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-fi/strings.xml b/pdf/pdf-viewer/src/main/res/values-fi/strings.xml
index 3518c19..bde5225 100644
--- a/pdf/pdf-viewer/src/main/res/values-fi/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-fi/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Tiedoston avaaminen epäonnistui. Mahdollinen lupaan liittyvä ongelma?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF-dokumenttiin liittyvä sivu on rikki"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Riittämätön data PDF-dokumentin käsittelyyn"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF-tiedostoa ei voi avata"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-fr-rCA/strings.xml b/pdf/pdf-viewer/src/main/res/values-fr-rCA/strings.xml
index 8a25992..7e7ac2c 100644
--- a/pdf/pdf-viewer/src/main/res/values-fr-rCA/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-fr-rCA/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Échec de l\'ouverture du fichier. Problème d\'autorisation éventuel?"</string>
<string name="page_broken" msgid="2968770793669433462">"Page brisée pour le document PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Données insuffisantes pour le traitement du document PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Impossible d\'ouvrir le fichier PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-fr/strings.xml b/pdf/pdf-viewer/src/main/res/values-fr/strings.xml
index dd5fcad..ff3027c 100644
--- a/pdf/pdf-viewer/src/main/res/values-fr/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-fr/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Échec de l\'ouverture du fichier. Problème d\'autorisation possible ?"</string>
<string name="page_broken" msgid="2968770793669433462">"Page non fonctionnelle pour le document PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Données insuffisantes pour le traitement du document PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Impossible d\'ouvrir le fichier PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-gl/strings.xml b/pdf/pdf-viewer/src/main/res/values-gl/strings.xml
index ebe0816..9cfb9c7 100644
--- a/pdf/pdf-viewer/src/main/res/values-gl/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-gl/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Produciuse un erro ao abrir o ficheiro. É posible que haxa problemas co permiso?"</string>
<string name="page_broken" msgid="2968770793669433462">"Non funciona a páxina para o documento PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Os datos non son suficientes para procesar o documento PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Non se puido abrir o PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-gu/strings.xml b/pdf/pdf-viewer/src/main/res/values-gu/strings.xml
index ae95929..f3dfb43 100644
--- a/pdf/pdf-viewer/src/main/res/values-gu/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-gu/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"ફાઇલ ખોલવામાં નિષ્ફળ રહ્યાં. શું તમારી પાસે આની પરવાનગી નથી?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF દસ્તાવેજ માટે પેજ લોડ થઈ રહ્યું નથી"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF દસ્તાવેજ પર પ્રક્રિયા કરવા માટે પર્યાપ્ત ડેટા નથી"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF ફાઇલ ખોલી શકાતી નથી"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-hr/strings.xml b/pdf/pdf-viewer/src/main/res/values-hr/strings.xml
index 7248c6ee..71cafc1 100644
--- a/pdf/pdf-viewer/src/main/res/values-hr/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-hr/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Otvaranje datoteke nije uspjelo. Možda postoji problem s dopuštenjem?"</string>
<string name="page_broken" msgid="2968770793669433462">"Stranica je raščlanjena za PDF dokument"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Nema dovoljno podataka za obradu PDF dokumenta"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF datoteka ne može se otvoriti"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-hu/strings.xml b/pdf/pdf-viewer/src/main/res/values-hu/strings.xml
index f63a921..df1766c 100644
--- a/pdf/pdf-viewer/src/main/res/values-hu/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-hu/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Nem sikerült megnyitni a fájlt. Engedéllyel kapcsolatos problémáról lehet szó?"</string>
<string name="page_broken" msgid="2968770793669433462">"Az oldal nem tölt be a PDF-dokumentumban"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Nem áll rendelkezésre elegendő adat a PDF-dokumentum feldolgozásához"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Nem sikerült megnyitni a PDF-fájlt"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-hy/strings.xml b/pdf/pdf-viewer/src/main/res/values-hy/strings.xml
index 71d24c2..5faf6897 100644
--- a/pdf/pdf-viewer/src/main/res/values-hy/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-hy/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Չհաջողվեց բացել ֆայլը։ Գուցե թույլտվության հետ կապված խնդի՞ր է։"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF փաստաթղթի էջը վնասված է"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Ոչ բավարար տվյալներ PDF փաստաթղթի մշակման համար"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Չհաջողվեց բացել PDF ֆայլը"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-in/strings.xml b/pdf/pdf-viewer/src/main/res/values-in/strings.xml
index 3c9e624..c71a4e2 100644
--- a/pdf/pdf-viewer/src/main/res/values-in/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-in/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Gagal membuka file. Kemungkinan masalah izin?"</string>
<string name="page_broken" msgid="2968770793669433462">"Halaman dokumen PDF rusak"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Data tidak cukup untuk memproses dokumen PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Tidak dapat membuka file PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-is/strings.xml b/pdf/pdf-viewer/src/main/res/values-is/strings.xml
index 9378e05..9fe38d7 100644
--- a/pdf/pdf-viewer/src/main/res/values-is/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-is/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Ekki tókst að opna skrána. Hugsanlega vandamál tengt heimildum?"</string>
<string name="page_broken" msgid="2968770793669433462">"Síða í PDF-skjali er gölluð"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Ekki næg gögn fyrir úrvinnslu á PDF-skjali"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Ekki tókst að opna PDF-skrá"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-it/strings.xml b/pdf/pdf-viewer/src/main/res/values-it/strings.xml
index 89a8d17..e2acf03 100644
--- a/pdf/pdf-viewer/src/main/res/values-it/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-it/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Impossibile aprire il file. Possibile problema di autorizzazione?"</string>
<string name="page_broken" msgid="2968770793669433462">"Pagina inaccessibile per il documento PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Dati insufficienti per l\'elaborazione del documento PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Impossibile aprire il file PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-iw/strings.xml b/pdf/pdf-viewer/src/main/res/values-iw/strings.xml
index 3dfd510..a450133 100644
--- a/pdf/pdf-viewer/src/main/res/values-iw/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-iw/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"לא ניתן לפתוח את הקובץ. יכול להיות שיש בעיה בהרשאה."</string>
<string name="page_broken" msgid="2968770793669433462">"קישור מנותק בדף למסמך ה-PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"אין מספיק נתונים כדי לעבד את מסמך ה-PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"לא ניתן לפתוח את קובץ ה-PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-kk/strings.xml b/pdf/pdf-viewer/src/main/res/values-kk/strings.xml
index 5846202..01429a0 100644
--- a/pdf/pdf-viewer/src/main/res/values-kk/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-kk/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Файл ашылмады. Бәлкім, рұқсатқа қатысты бір мәселе бар?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF құжатының беті бұзылған."</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF құжатын өңдеу үшін деректер жеткіліксіз."</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF файлын ашу мүмкін емес."</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-km/strings.xml b/pdf/pdf-viewer/src/main/res/values-km/strings.xml
index 931c43f..ecde5c2 100644
--- a/pdf/pdf-viewer/src/main/res/values-km/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-km/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"មិនអាចបើកឯកសារនេះបានទេ។ អាចមានបញ្ហានៃការអនុញ្ញាតឬ?"</string>
<string name="page_broken" msgid="2968770793669433462">"ទំព័រមិនដំណើរការសម្រាប់ឯកសារ PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"មានទិន្នន័យមិនគ្រប់គ្រាន់សម្រាប់ដំណើរការឯកសារ PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"មិនអាចបើកឯកសារ PDF បានទេ"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-kn/strings.xml b/pdf/pdf-viewer/src/main/res/values-kn/strings.xml
index fb8b009..c023a93 100644
--- a/pdf/pdf-viewer/src/main/res/values-kn/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-kn/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"ಫೈಲ್ ತೆರೆಯಲು ವಿಫಲವಾಗಿದೆ. ಸಂಭವನೀಯ ಅನುಮತಿ ಸಮಸ್ಯೆ?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF ಡಾಕ್ಯುಮೆಂಟ್ಗೆ ಸಂಬಂಧಿಸಿದ ಪುಟ ಮುರಿದಿದೆ"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF ಡಾಕ್ಯುಮೆಂಟ್ ಅನ್ನು ಪ್ರಕ್ರಿಯೆಗೊಳಿಸಲು ಸಾಕಷ್ಟು ಡೇಟಾ ಇಲ್ಲ"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF ಫೈಲ್ ಅನ್ನು ತೆರೆಯಲು ಸಾಧ್ಯವಿಲ್ಲ"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-ko/strings.xml b/pdf/pdf-viewer/src/main/res/values-ko/strings.xml
index 9530691..766dd73 100644
--- a/pdf/pdf-viewer/src/main/res/values-ko/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-ko/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"파일을 열 수 없습니다. 권한 문제가 있을 수 있나요?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF 문서의 페이지가 손상되었습니다."</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF 문서 처리를 위한 데이터가 부족합니다."</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF 파일을 열 수 없음"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-ky/strings.xml b/pdf/pdf-viewer/src/main/res/values-ky/strings.xml
index 92e5119..ef64372 100644
--- a/pdf/pdf-viewer/src/main/res/values-ky/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-ky/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Файл ачылган жок. Керектүү уруксаттар жок окшойт."</string>
<string name="page_broken" msgid="2968770793669433462">"PDF документинин барагы бузук"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF документин иштетүү үчүн маалымат жетишсиз"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF файл ачылбай жатат"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-lo/strings.xml b/pdf/pdf-viewer/src/main/res/values-lo/strings.xml
index baa1c28..f497820 100644
--- a/pdf/pdf-viewer/src/main/res/values-lo/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-lo/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"ເປີດໄຟລ໌ບໍ່ສຳເລັດ. ອາດເປັນຍ້ອນບັນຫາທາງການອະນຸຍາດບໍ?"</string>
<string name="page_broken" msgid="2968770793669433462">"ໜ້າເສຍຫາຍສໍາລັບເອກະສານ PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"ຂໍ້ມູນບໍ່ພຽງພໍສໍາລັບການປະມວນຜົນເອກະສານ PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"ບໍ່ສາມາດເປີດໄຟລ໌ PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-lt/strings.xml b/pdf/pdf-viewer/src/main/res/values-lt/strings.xml
index 588ff87..6c3088c 100644
--- a/pdf/pdf-viewer/src/main/res/values-lt/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-lt/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Nepavyko atidaryti failo. Galima su leidimais susijusi problema?"</string>
<string name="page_broken" msgid="2968770793669433462">"Sugadintas PDF dokumento puslapis"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Nepakanka duomenų PDF dokumentui apdoroti"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Nepavyksta atidaryti PDF failo"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-lv/strings.xml b/pdf/pdf-viewer/src/main/res/values-lv/strings.xml
index 738615d..a1d28ba 100644
--- a/pdf/pdf-viewer/src/main/res/values-lv/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-lv/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Neizdevās atvērt failu. Iespējams, ir radusies problēma ar atļaujām."</string>
<string name="page_broken" msgid="2968770793669433462">"PDF dokumenta lapa ir bojāta"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Nepietiekams datu apjoms, lai apstrādātu PDF dokumentu"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Nevar atvērt PDF failu."</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-mk/strings.xml b/pdf/pdf-viewer/src/main/res/values-mk/strings.xml
index e6b85c2..3d48afd 100644
--- a/pdf/pdf-viewer/src/main/res/values-mk/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-mk/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Не можеше да се отвори датотеката. Можеби има проблем со дозволата?"</string>
<string name="page_broken" msgid="2968770793669433462">"Страницата не може да го вчита PDF-документот"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Недоволно податоци за обработка на PDF-документот"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Не може да се отвори PDF-датотеката"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-ml/strings.xml b/pdf/pdf-viewer/src/main/res/values-ml/strings.xml
index 760a696..d8bf511 100644
--- a/pdf/pdf-viewer/src/main/res/values-ml/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-ml/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"ഫയൽ തുറക്കാനായില്ല. അനുമതി സംബന്ധിച്ച പ്രശ്നമാകാൻ സാധ്യതയുണ്ടോ?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF ഡോക്യുമെന്റിനായി പേജ് ലോഡ് ചെയ്യാനായില്ല"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF ഡോക്യുമെന്റ് പ്രോസസ് ചെയ്യാൻ മതിയായ ഡാറ്റയില്ല"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF ഫയൽ തുറക്കാനാകുന്നില്ല"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-mn/strings.xml b/pdf/pdf-viewer/src/main/res/values-mn/strings.xml
index 9c8f052..04d4944 100644
--- a/pdf/pdf-viewer/src/main/res/values-mn/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-mn/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Файлыг нээж чадсангүй. Зөвшөөрөлтэй холбоотой асуудал байж болох уу?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF баримт бичгийн хуудас эвдэрсэн"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF баримт бичгийг боловсруулахад өгөгдөл хангалтгүй байна"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF файлыг нээх боломжгүй"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-mr/strings.xml b/pdf/pdf-viewer/src/main/res/values-mr/strings.xml
index 40ee1a8..3c3c6de 100644
--- a/pdf/pdf-viewer/src/main/res/values-mr/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-mr/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"फाइल उघडता आली नाही. परवानगीशी संबंधित संभाव्य समस्या?"</string>
<string name="page_broken" msgid="2968770793669433462">"पीडीएफ दस्तऐवजासाठी पेज खंडित झाले आहे"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF दस्तऐवजावर प्रक्रिया करण्यासाठी डेटा पुरेसा नाही"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF फाइल उघडू शकत नाही"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-ms/strings.xml b/pdf/pdf-viewer/src/main/res/values-ms/strings.xml
index f905244..af16379 100644
--- a/pdf/pdf-viewer/src/main/res/values-ms/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-ms/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Gagal membuka fail. Kemungkinan terdapat masalah berkaitan dengan kebenaran?"</string>
<string name="page_broken" msgid="2968770793669433462">"Halaman rosak untuk dokumen PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Data tidak mencukupi untuk memproses dokumen PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Tidak dapat membuka fail PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-my/strings.xml b/pdf/pdf-viewer/src/main/res/values-my/strings.xml
index c288df3..27f063f 100644
--- a/pdf/pdf-viewer/src/main/res/values-my/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-my/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"ဖိုင်ကို ဖွင့်၍မရလိုက်ပါ။ ခွင့်ပြုချက် ပြဿနာ ဖြစ်နိုင်လား။"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF မှတ်တမ်းအတွက် စာမျက်နှာ ပျက်နေသည်"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF မှတ်တမ်း လုပ်ဆောင်ရန်အတွက် ဒေတာ မလုံလောက်ပါ"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF ဖိုင်ကို ဖွင့်၍မရပါ"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-nb/strings.xml b/pdf/pdf-viewer/src/main/res/values-nb/strings.xml
index 06e5547..64bb49b 100644
--- a/pdf/pdf-viewer/src/main/res/values-nb/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-nb/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Kunne ikke åpne filen. Kan det være et problem med tillatelser?"</string>
<string name="page_broken" msgid="2968770793669433462">"Siden er ødelagt for PDF-dokumentet"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Det er utilstrekkelige data for behandling av PDF-dokumentet"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Kan ikke åpne PDF-filen"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-ne/strings.xml b/pdf/pdf-viewer/src/main/res/values-ne/strings.xml
index d847ed0c..109c214 100644
--- a/pdf/pdf-viewer/src/main/res/values-ne/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-ne/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"फाइल खोल्न सकिएन। तपाईंसँग यो फाइल खोल्ने अनुमति छैन?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF डकुमेन्टको पेज लोड गर्न सकिएन"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF डकुमेन्ट प्रोसेस गर्न पर्याप्त जानकारी छैन"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF फाइल खोल्न सकिएन"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-nl/strings.xml b/pdf/pdf-viewer/src/main/res/values-nl/strings.xml
index 5ceea0c..08eb1bd5 100644
--- a/pdf/pdf-viewer/src/main/res/values-nl/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-nl/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Kan het bestand niet openen. Mogelijk rechtenprobleem?"</string>
<string name="page_broken" msgid="2968770793669433462">"Pagina van het pdf-document kan niet worden geladen"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Onvoldoende gegevens om het pdf-document te verwerken"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Kan pdf-bestand niet openen"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-or/strings.xml b/pdf/pdf-viewer/src/main/res/values-or/strings.xml
index 66b7f68..9623c13 100644
--- a/pdf/pdf-viewer/src/main/res/values-or/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-or/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"ଫାଇଲ ଖୋଲିବାରେ ବିଫଳ ହୋଇଛି। ସମ୍ଭାବ୍ୟ ଅନୁମତି ସମସ୍ୟା ଅଛି?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF ଡକ୍ୟୁମେଣ୍ଟ ପାଇଁ ପୃଷ୍ଠା ବିଭାଜିତ ହୋଇଛି"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF ଡକ୍ୟୁମେଣ୍ଟ ପ୍ରକ୍ରିୟାକରଣ ପାଇଁ ପର୍ଯ୍ୟାପ୍ତ ଡାଟା ନାହିଁ"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF ଫାଇଲକୁ ଖୋଲାଯାଇପାରିବ ନାହିଁ"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-pa/strings.xml b/pdf/pdf-viewer/src/main/res/values-pa/strings.xml
index 8d4c105..edab9fc 100644
--- a/pdf/pdf-viewer/src/main/res/values-pa/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-pa/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"ਫ਼ਾਈਲ ਨੂੰ ਖੋਲ੍ਹਣਾ ਅਸਫਲ ਰਿਹਾ। ਕੀ ਸੰਭਵ ਇਜਾਜ਼ਤ ਸੰਬੰਧੀ ਸਮੱਸਿਆ ਹੈ?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF ਦਸਤਾਵੇਜ਼ ਲਈ ਪੰਨਾ ਲੋਡ ਨਹੀਂ ਹੋ ਰਿਹਾ"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF ਦਸਤਾਵੇਜ਼ \'ਤੇ ਪ੍ਰਕਿਰਿਆ ਕਰਨ ਲਈ ਲੋੜੀਂਦਾ ਡਾਟਾ ਨਹੀਂ ਹੈ"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF ਫ਼ਾਈਲ ਖੋਲ੍ਹੀ ਨਹੀਂ ਜਾ ਸਕਦੀ"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-pl/strings.xml b/pdf/pdf-viewer/src/main/res/values-pl/strings.xml
index 6583cb5..baa168f 100644
--- a/pdf/pdf-viewer/src/main/res/values-pl/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-pl/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Nie udało się otworzyć pliku. Może to przez problem z uprawnieniami?"</string>
<string name="page_broken" msgid="2968770793669433462">"Strona w dokumencie PDF jest uszkodzona"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Brak wystarczającej ilości danych do przetworzenia dokumentu PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Nie można otworzyć pliku PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-pt-rBR/strings.xml b/pdf/pdf-viewer/src/main/res/values-pt-rBR/strings.xml
index e8beb95..6c852a6 100644
--- a/pdf/pdf-viewer/src/main/res/values-pt-rBR/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-pt-rBR/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Falha ao abrir o arquivo. Possível problema de permissão?"</string>
<string name="page_broken" msgid="2968770793669433462">"Página do documento PDF corrompida"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Dados insuficientes para processamento do documento PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Não é possível abrir o arquivo PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-pt/strings.xml b/pdf/pdf-viewer/src/main/res/values-pt/strings.xml
index e8beb95..6c852a6 100644
--- a/pdf/pdf-viewer/src/main/res/values-pt/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-pt/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Falha ao abrir o arquivo. Possível problema de permissão?"</string>
<string name="page_broken" msgid="2968770793669433462">"Página do documento PDF corrompida"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Dados insuficientes para processamento do documento PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Não é possível abrir o arquivo PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-ro/strings.xml b/pdf/pdf-viewer/src/main/res/values-ro/strings.xml
index ef6b73d..43bec5af 100644
--- a/pdf/pdf-viewer/src/main/res/values-ro/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-ro/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Nu s-a putut deschide fișierul. Există vreo problemă cu permisiunile?"</string>
<string name="page_broken" msgid="2968770793669433462">"Pagină deteriorată pentru documentul PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Date insuficiente pentru procesarea documentului PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Nu se poate deschide fișierul PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-ru/strings.xml b/pdf/pdf-viewer/src/main/res/values-ru/strings.xml
index 665e7cb..7aec0cd 100644
--- a/pdf/pdf-viewer/src/main/res/values-ru/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-ru/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Не удалось открыть файл. Возможно, нет необходимых разрешений."</string>
<string name="page_broken" msgid="2968770793669433462">"Страница документа PDF повреждена"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Недостаточно данных для обработки документа PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Не удается открыть PDF-файл."</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-si/strings.xml b/pdf/pdf-viewer/src/main/res/values-si/strings.xml
index dc5d075..482776b8 100644
--- a/pdf/pdf-viewer/src/main/res/values-si/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-si/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"ගොනුව විවෘත කිරීමට අසමත් විය. අවසර ගැටලුවක් විය හැකි ද?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF ලේඛනය සඳහා පිටුව හානි වී ඇත"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF ලේඛනය සැකසීම සඳහා ප්රමාණවත් දත්ත නොමැත"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF ගොනුව විවෘත කළ නොහැක"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-sk/strings.xml b/pdf/pdf-viewer/src/main/res/values-sk/strings.xml
index d44c7b6..e214804 100644
--- a/pdf/pdf-viewer/src/main/res/values-sk/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-sk/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Súbor sa nepodarilo otvoriť. Možno sa vyskytol problém s povolením."</string>
<string name="page_broken" msgid="2968770793669433462">"Stránka sa v dokumente vo formáte PDF nedá načítať"</string>
<string name="needs_more_data" msgid="3520133467908240802">"V dokumente vo formáte PDF nie je dostatok údajov na spracovanie"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Súbor PDF sa nepodarilo otvoriť"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-sq/strings.xml b/pdf/pdf-viewer/src/main/res/values-sq/strings.xml
index 37347d3..ae8b533 100644
--- a/pdf/pdf-viewer/src/main/res/values-sq/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-sq/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Hapja e skedarit dështoi. Problem i mundshëm me lejet?"</string>
<string name="page_broken" msgid="2968770793669433462">"Faqe e dëmtuar për dokumentin PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Të dhëna të pamjaftueshme për përpunimin e dokumentit PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Skedari PDF nuk mund të hapet"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-sr/strings.xml b/pdf/pdf-viewer/src/main/res/values-sr/strings.xml
index bcab510..9de9525 100644
--- a/pdf/pdf-viewer/src/main/res/values-sr/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-sr/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Отварање фајла није успело. Можда постоје проблеми са дозволом?"</string>
<string name="page_broken" msgid="2968770793669433462">"Неисправна страница за PDF документ"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Недовољно података за обраду PDF документа"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Отварање PDF фајла није успело"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-sv/strings.xml b/pdf/pdf-viewer/src/main/res/values-sv/strings.xml
index 3fa349f..186bb52 100644
--- a/pdf/pdf-viewer/src/main/res/values-sv/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-sv/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Det gick inte att öppna filen. Detta kan bero på ett behörighetsproblem."</string>
<string name="page_broken" msgid="2968770793669433462">"Det gick inte att läsa in en sida i PDF-dokumentet"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Otillräcklig data för att behandla PDF-dokumentet"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Det går inte att öppna PDF-filen"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-sw/strings.xml b/pdf/pdf-viewer/src/main/res/values-sw/strings.xml
index abf9c4b5..0a46009 100644
--- a/pdf/pdf-viewer/src/main/res/values-sw/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-sw/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Imeshindwa kufungua faili. Je, linaweza kuwa tatizo la ruhusa?"</string>
<string name="page_broken" msgid="2968770793669433462">"Ukurasa wa hati ya PDF una tatizo"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Hamna data ya kutosha kuchakata hati ya PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Imeshindwa kufungua faili ya PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-ta/strings.xml b/pdf/pdf-viewer/src/main/res/values-ta/strings.xml
index 2d1d291..fe6db7a 100644
--- a/pdf/pdf-viewer/src/main/res/values-ta/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-ta/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"ஃபைலைத் திறக்க முடியவில்லை. அனுமதி தொடர்பான சிக்கல் உள்ளதா?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF ஆவணத்தை ஏற்ற முடியவில்லை"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF ஆவணத்தைச் செயலாக்குவதற்குப் போதுமான தரவு இல்லை"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF ஃபைலைத் திறக்க முடியவில்லை"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-tr/strings.xml b/pdf/pdf-viewer/src/main/res/values-tr/strings.xml
index 8ad6bf2..916c0e0 100644
--- a/pdf/pdf-viewer/src/main/res/values-tr/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-tr/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Dosya açılamadı. İzin sorunundan kaynaklanıyor olabilir mi?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF dokümanının sayfası bozuk"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF dokümanını işleyecek kadar yeterli veri yok"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF dosyası açılamıyor"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-uk/strings.xml b/pdf/pdf-viewer/src/main/res/values-uk/strings.xml
index c381f20..f413bfc 100644
--- a/pdf/pdf-viewer/src/main/res/values-uk/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-uk/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Не вдалося відкрити файл. Можливо, виникла проблема з дозволом."</string>
<string name="page_broken" msgid="2968770793669433462">"Сторінку документа PDF пошкоджено"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Недостатньо даних для обробки документа PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Не вдалося відкрити файл PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-ur/strings.xml b/pdf/pdf-viewer/src/main/res/values-ur/strings.xml
index 6027e36..f2c3bd2 100644
--- a/pdf/pdf-viewer/src/main/res/values-ur/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-ur/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"فائل کھولنے میں ناکام۔ کیا یہ اجازت کا مسئلہ ہو سکتا ہے؟"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF دستاویز کیلئے شکستہ صفحہ"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF دستاویز پر کارروائی کرنے کیلئے ڈیٹا ناکافی ہے"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF فائل کو کھولا نہیں جا سکتا"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-vi/strings.xml b/pdf/pdf-viewer/src/main/res/values-vi/strings.xml
index 5f50634..165db32 100644
--- a/pdf/pdf-viewer/src/main/res/values-vi/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-vi/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Không mở được tệp này. Có thể là do vấn đề về quyền?"</string>
<string name="page_broken" msgid="2968770793669433462">"Tài liệu PDF này bị lỗi trang"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Không đủ dữ liệu để xử lý tài liệu PDF này"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Không mở được tệp PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-zh-rHK/strings.xml b/pdf/pdf-viewer/src/main/res/values-zh-rHK/strings.xml
index 691bdfe..9eb4c8f 100644
--- a/pdf/pdf-viewer/src/main/res/values-zh-rHK/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-zh-rHK/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"無法開啟檔案。可能有權限問題?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF 文件頁面已損毀"</string>
<string name="needs_more_data" msgid="3520133467908240802">"沒有足夠資料處理 PDF 文件"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"無法開啟 PDF 檔案"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-zh-rTW/strings.xml b/pdf/pdf-viewer/src/main/res/values-zh-rTW/strings.xml
index b2be3f4..daaadee 100644
--- a/pdf/pdf-viewer/src/main/res/values-zh-rTW/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-zh-rTW/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"無法開啟檔案。有可能是權限問題?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF 文件的頁面損毀"</string>
<string name="needs_more_data" msgid="3520133467908240802">"資料不足,無法處理 PDF 文件"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"無法開啟 PDF 檔案"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-zu/strings.xml b/pdf/pdf-viewer/src/main/res/values-zu/strings.xml
index ba456e5..e58cf20 100644
--- a/pdf/pdf-viewer/src/main/res/values-zu/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-zu/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Yehlulekile ukuvula ifayela. Inkinga yemvume engaba khona?"</string>
<string name="page_broken" msgid="2968770793669433462">"Ikhasi eliphuliwe ledokhumenti ye-PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Idatha enganele yokucubungula idokhumenti ye-PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Ayikwazi ukuvula ifayela le-PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/dimensions.xml b/pdf/pdf-viewer/src/main/res/values/dimensions.xml
index 03bf958..584fda6 100644
--- a/pdf/pdf-viewer/src/main/res/values/dimensions.xml
+++ b/pdf/pdf-viewer/src/main/res/values/dimensions.xml
@@ -15,7 +15,6 @@
-->
<resources>
- <dimen name="viewer_doc_additional_top_offset">12dp</dimen>
<dimen name="viewer_fastscroll_edge_offset">24dp</dimen>
<dimen name="viewer_doc_padding_y">4dp</dimen>
<dimen name="viewer_doc_padding_x">0dp</dimen>
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageSelectionValueObserverTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageSelectionValueObserverTest.java
index 384ab67..9c4b2a6 100644
--- a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageSelectionValueObserverTest.java
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageSelectionValueObserverTest.java
@@ -78,7 +78,7 @@
PdfViewer.setScreenForTest(mContext);
PageSelectionValueObserver pageSelectionValueObserver =
- new PageSelectionValueObserver(mMockPaginatedView, mMockPaginationModel,
+ new PageSelectionValueObserver(mMockPaginatedView,
mMockPageViewFactory, mContext);
pageSelectionValueObserver.onChange(mMockOldPageSelection, mMockNewPageSelection);
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/SelectedMatchValueObserverTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/SelectedMatchValueObserverTest.java
index 9c15f78..1ef67d9 100644
--- a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/SelectedMatchValueObserverTest.java
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/SelectedMatchValueObserverTest.java
@@ -89,7 +89,7 @@
PdfViewer.setScreenForTest(mContext);
SelectedMatchValueObserver selectedMatchValueObserver = new SelectedMatchValueObserver(
- mMockPaginatedView, mMockPaginationModel, mMockPageViewFactory, mMockZoomView,
+ mMockPaginatedView, mMockPageViewFactory, mMockZoomView,
mMockLayoutHandler, mContext);
selectedMatchValueObserver.onChange(mMockOldSelection, mMockNewSelection);
diff --git a/preference/preference/build.gradle b/preference/preference/build.gradle
index 27d3503..d3140ab 100644
--- a/preference/preference/build.gradle
+++ b/preference/preference/build.gradle
@@ -38,8 +38,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.kotlinStdlib)
androidTestImplementation(libs.truth)
}
diff --git a/privacysandbox/ads/ads-adservices-java/build.gradle b/privacysandbox/ads/ads-adservices-java/build.gradle
index 305ee1a..c38dc28 100644
--- a/privacysandbox/ads/ads-adservices-java/build.gradle
+++ b/privacysandbox/ads/ads-adservices-java/build.gradle
@@ -53,8 +53,8 @@
androidTestImplementation(libs.truth)
androidTestImplementation(project(":internal-testutils-truth"))
- androidTestImplementation(libs.mockitoCore4, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockitoInline, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore4)
+ androidTestImplementation(libs.dexmakerMockitoInline)
androidTestImplementation(libs.dexmakerMockitoInlineExtended)
}
diff --git a/privacysandbox/ads/ads-adservices/build.gradle b/privacysandbox/ads/ads-adservices/build.gradle
index 5610d1a..afa4d60 100644
--- a/privacysandbox/ads/ads-adservices/build.gradle
+++ b/privacysandbox/ads/ads-adservices/build.gradle
@@ -45,8 +45,8 @@
androidTestImplementation(libs.truth)
androidTestImplementation(project(":internal-testutils-truth"))
- androidTestImplementation(libs.mockitoCore4, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockitoInline, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore4)
+ androidTestImplementation(libs.dexmakerMockitoInline)
androidTestImplementation(libs.dexmakerMockitoInlineExtended)
}
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/build.gradle b/privacysandbox/sdkruntime/sdkruntime-client/build.gradle
index fb5e7c0..8753f06 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/build.gradle
+++ b/privacysandbox/sdkruntime/sdkruntime-client/build.gradle
@@ -120,8 +120,8 @@
androidTestImplementation(project(':internal-testutils-runtime'))
androidTestImplementation(project(":internal-testutils-truth")) // for assertThrows
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockitoInline, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockitoInline)
androidTestBundleDex(project(":privacysandbox:sdkruntime:test-sdks:current"))
androidTestBundleDex(project(":privacysandbox:sdkruntime:test-sdks:v4"))
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/build.gradle b/privacysandbox/sdkruntime/sdkruntime-core/build.gradle
index a8366d3..21ed9ac 100644
--- a/privacysandbox/sdkruntime/sdkruntime-core/build.gradle
+++ b/privacysandbox/sdkruntime/sdkruntime-core/build.gradle
@@ -45,8 +45,8 @@
androidTestImplementation(libs.truth)
androidTestImplementation(libs.junit)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockitoInline, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockitoInline)
}
android {
diff --git a/privacysandbox/sdkruntime/sdkruntime-provider/build.gradle b/privacysandbox/sdkruntime/sdkruntime-provider/build.gradle
index 83865d4..3e22512 100644
--- a/privacysandbox/sdkruntime/sdkruntime-provider/build.gradle
+++ b/privacysandbox/sdkruntime/sdkruntime-provider/build.gradle
@@ -38,8 +38,8 @@
androidTestImplementation(libs.truth)
androidTestImplementation(libs.junit)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockitoInline, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockitoInline)
}
android {
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt
index 86fed41..79fdec1 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt
@@ -17,8 +17,11 @@
package androidx.privacysandbox.ui.integration.testapp
import android.app.Activity
+import android.graphics.Color
+import android.graphics.Typeface
import android.os.Bundle
import android.util.Log
+import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
@@ -59,6 +62,10 @@
}
}
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ getSandboxedSdkViews().forEach { it.addStateChangedListener() }
+ }
+
/** Returns a handle to the already loaded SDK. */
fun getSdkApi(): ISdkApi {
return sdkApi
@@ -119,6 +126,8 @@
val parent = view.parent as ViewGroup
val index = parent.indexOfChild(view)
val textView = TextView(requireActivity())
+ textView.setTypeface(null, Typeface.BOLD_ITALIC)
+ textView.setTextColor(Color.RED)
textView.text = state.throwable.message
requireActivity().runOnUiThread {
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt
index 02d4cec..b64294d 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt
@@ -285,15 +285,15 @@
if (!isDrawerOpen) {
isDrawerOpen = true
currentFragment.handleDrawerStateChange(isDrawerOpen = true)
+ } else if (slideOffset == 0f) {
+ isDrawerOpen = false
+ currentFragment.handleDrawerStateChange(isDrawerOpen = false)
}
}
override fun onDrawerOpened(drawerView: View) {}
- override fun onDrawerClosed(drawerView: View) {
- isDrawerOpen = false
- currentFragment.handleDrawerStateChange(isDrawerOpen = false)
- }
+ override fun onDrawerClosed(drawerView: View) {}
override fun onDrawerStateChanged(newState: Int) {}
}
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/PoolingContainerFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/PoolingContainerFragment.kt
index 4a9ccf7..d3eba3b 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/PoolingContainerFragment.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/PoolingContainerFragment.kt
@@ -104,6 +104,7 @@
} catch (e: Exception) {
Log.w(TAG, "Ad not loaded $e")
}
+ childSandboxedSdkView.addStateChangedListener()
sandboxedSdkViewSet.add(childSandboxedSdkView)
}
}
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ResizeFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ResizeFragment.kt
index 3a3f32b..166d109 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ResizeFragment.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ResizeFragment.kt
@@ -67,7 +67,6 @@
}
private fun loadResizableBannerAd() {
- resizableBannerView.addStateChangedListener()
loadBannerAd(
currentAdType,
currentMediationOption,
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ScrollFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ScrollFragment.kt
index e49be51..2d38fe7 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ScrollFragment.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ScrollFragment.kt
@@ -61,7 +61,6 @@
}
private fun loadBottomBannerAd() {
- bottomBannerView.addStateChangedListener()
bottomBannerView.layoutParams =
inflatedView.findViewById<LinearLayout>(R.id.bottom_banner_container).layoutParams
requireActivity().runOnUiThread {
diff --git a/profileinstaller/profileinstaller-benchmark/build.gradle b/profileinstaller/profileinstaller-benchmark/build.gradle
index 8646ff0..d813b34 100644
--- a/profileinstaller/profileinstaller-benchmark/build.gradle
+++ b/profileinstaller/profileinstaller-benchmark/build.gradle
@@ -32,14 +32,14 @@
dependencies {
androidTestImplementation(project(":profileinstaller:profileinstaller"))
- androidTestImplementation(projectOrArtifact(":benchmark:benchmark-junit4"))
+ androidTestImplementation(project(":benchmark:benchmark-junit4"))
androidTestImplementation(libs.junit)
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation project(':internal-testutils-runtime')
androidTestImplementation(libs.kotlinStdlib)
}
diff --git a/recyclerview/recyclerview-selection/build.gradle b/recyclerview/recyclerview-selection/build.gradle
index b599c61..0768c45 100644
--- a/recyclerview/recyclerview-selection/build.gradle
+++ b/recyclerview/recyclerview-selection/build.gradle
@@ -38,8 +38,8 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it's own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it's own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.junit)
}
diff --git a/recyclerview/recyclerview/build.gradle b/recyclerview/recyclerview/build.gradle
index 92b9d6f..395de30 100644
--- a/recyclerview/recyclerview/build.gradle
+++ b/recyclerview/recyclerview/build.gradle
@@ -26,8 +26,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.truth)
androidTestImplementation(libs.junit)
androidTestImplementation(libs.kotlinStdlib)
diff --git a/remotecallback/remotecallback/build.gradle b/remotecallback/remotecallback/build.gradle
index c1da69a..5cd252f 100644
--- a/remotecallback/remotecallback/build.gradle
+++ b/remotecallback/remotecallback/build.gradle
@@ -35,8 +35,8 @@
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestAnnotationProcessor(project(":remotecallback:remotecallback-processor"))
}
diff --git a/room/benchmark/build.gradle b/room/benchmark/build.gradle
index c679882..146a376 100644
--- a/room/benchmark/build.gradle
+++ b/room/benchmark/build.gradle
@@ -37,7 +37,7 @@
kspAndroidTest project(":room:room-compiler")
androidTestImplementation(project(":room:room-rxjava2"))
androidTestImplementation("androidx.arch.core:core-runtime:2.2.0")
- androidTestImplementation(projectOrArtifact(":benchmark:benchmark-junit4"))
+ androidTestImplementation(project(":benchmark:benchmark-junit4"))
androidTestImplementation(libs.rxjava2)
androidTestImplementation(libs.junit)
androidTestImplementation(libs.testExtJunit)
diff --git a/room/integration-tests/autovaluetestapp/build.gradle b/room/integration-tests/autovaluetestapp/build.gradle
index 86c2124..3336f4f 100644
--- a/room/integration-tests/autovaluetestapp/build.gradle
+++ b/room/integration-tests/autovaluetestapp/build.gradle
@@ -35,8 +35,8 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.espressoCore)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it's own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it's own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
testImplementation(libs.junit)
}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt
index 9b8764b..3be749d 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt
@@ -230,6 +230,9 @@
fun getBookFlowable(bookId: String): Flowable<Book>
@Query("SELECT * FROM book WHERE bookId = :bookId")
+ fun getBookObservable(bookId: String): Observable<Book>
+
+ @Query("SELECT * FROM book WHERE bookId = :bookId")
fun getBookJavaOptional(bookId: String): java.util.Optional<Book>
@Query("SELECT * FROM book WHERE bookId = :bookId")
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/BoxedNonNullTypesTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/BoxedNonNullTypesTest.kt
index 1fa133b..e1b25e63 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/BoxedNonNullTypesTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/BoxedNonNullTypesTest.kt
@@ -27,7 +27,6 @@
import androidx.room.Query
import androidx.room.Room
import androidx.room.RoomDatabase
-import androidx.room.integration.kotlintestapp.RoomTestConfig
import androidx.room.integration.kotlintestapp.assumeKsp
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -138,14 +137,6 @@
@Test // repro for: b/211822920
fun getAsRx2ObservableUnknownNullabilityInCursor() {
- if (RoomTestConfig.isKsp) {
- // only in KSP we know the value is non-null, hence default to 0.
- // in RX, it would generate code that would return null and get filtered by RxRoom
- // Even though this becomes inconsistent between KSP and KAPT, the KSP path is more
- // consistent with the non-observable version of the query.
- assertThat(db.myDao().getAsRx2ObservableUnknownTypeInCursor().blockingFirst())
- .isEqualTo(0L)
- }
db.myDao().insert(MyEntity(9))
assertThat(db.myDao().getAsRx2ObservableUnknownTypeInCursor().blockingFirst()).isEqualTo(9L)
}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/RxJava2QueryTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/RxJava2QueryTest.kt
index 00a8fc7..4aae5e3 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/RxJava2QueryTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/RxJava2QueryTest.kt
@@ -27,7 +27,7 @@
class RxJava2QueryTest : TestDatabaseTest() {
@Test
- fun observeBooksById() {
+ fun observeBooksByIdFlowable() {
booksDao.addAuthors(TestUtil.AUTHOR_1)
booksDao.addPublishers(TestUtil.PUBLISHER)
booksDao.addBooks(TestUtil.BOOK_1)
@@ -39,6 +39,38 @@
}
@Test
+ fun observeBooksByIdFlowable_noBook() {
+ booksDao
+ .getBookFlowable(TestUtil.BOOK_1.bookId)
+ .test()
+ .also { drain() }
+ .assertNoErrors()
+ .assertNoValues()
+ }
+
+ @Test
+ fun observeBooksByIdObservable() {
+ booksDao.addAuthors(TestUtil.AUTHOR_1)
+ booksDao.addPublishers(TestUtil.PUBLISHER)
+ booksDao.addBooks(TestUtil.BOOK_1)
+ booksDao
+ .getBookObservable(TestUtil.BOOK_1.bookId)
+ .test()
+ .also { drain() }
+ .assertValue { book -> book == TestUtil.BOOK_1 }
+ }
+
+ @Test
+ fun observeBooksById_noBook() {
+ booksDao
+ .getBookObservable(TestUtil.BOOK_1.bookId)
+ .test()
+ .also { drain() }
+ .assertNoErrors()
+ .assertNoValues()
+ }
+
+ @Test
fun observeBooksByIdSingle() {
booksDao.addAuthors(TestUtil.AUTHOR_1)
booksDao.addPublishers(TestUtil.PUBLISHER)
@@ -76,10 +108,9 @@
booksDao.addPublishers(TestUtil.PUBLISHER)
booksDao.addBooks(TestUtil.BOOK_1)
- var expected =
+ val expected =
BookWithPublisher(TestUtil.BOOK_1.bookId, TestUtil.BOOK_1.title, TestUtil.PUBLISHER)
- var expectedList = ArrayList<BookWithPublisher>()
- expectedList.add(expected)
+ val expectedList = listOf(expected)
booksDao.getBooksWithPublisherFlowable().test().also { drain() }.assertValue(expectedList)
}
diff --git a/room/integration-tests/testapp/build.gradle b/room/integration-tests/testapp/build.gradle
index 86b9003..6834e9b 100644
--- a/room/integration-tests/testapp/build.gradle
+++ b/room/integration-tests/testapp/build.gradle
@@ -102,7 +102,7 @@
androidTestImplementation(project(":room:room-guava"))
androidTestImplementation(project(":room:room-paging"))
androidTestImplementation("androidx.arch.core:core-testing:2.2.0")
- androidTestImplementation(projectOrArtifact(":paging:paging-runtime"))
+ androidTestImplementation(project(":paging:paging-runtime"))
androidTestImplementation("androidx.lifecycle:lifecycle-runtime:2.6.1")
androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.6.1")
androidTestImplementation("androidx.lifecycle:lifecycle-livedata:2.6.1")
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/ElementExt.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/ElementExt.kt
index 325d436..4fe0f34 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/ElementExt.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/ElementExt.kt
@@ -25,6 +25,7 @@
import javax.lang.model.element.ExecutableElement
import javax.lang.model.element.Modifier
import javax.lang.model.element.TypeElement
+import javax.lang.model.type.ExecutableType
import javax.lang.model.util.Types
import kotlin.coroutines.Continuation
@@ -42,14 +43,24 @@
}
internal val Element.nullability: XNullability
- get() =
- if (asType().kind.isPrimitive || hasAnyOf(NONNULL_ANNOTATIONS)) {
+ get() {
+ // Get the type of the element: if this is a method, use the return type instead of the full
+ // method type since the return is what determines nullability.
+ val asType =
+ asType().let {
+ when (it) {
+ is ExecutableType -> it.returnType
+ else -> it
+ }
+ }
+ return if (asType.kind.isPrimitive || hasAnyOf(NONNULL_ANNOTATIONS)) {
XNullability.NONNULL
} else if (hasAnyOf(NULLABLE_ANNOTATIONS)) {
XNullability.NULLABLE
} else {
XNullability.UNKNOWN
}
+ }
internal fun Element.requireEnclosingType(env: JavacProcessingEnv): JavacTypeElement {
return checkNotNull(enclosingType(env)) { "Cannot find required enclosing type for $this" }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
index 54f1708..e8c823f 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
@@ -210,3 +210,18 @@
val parent = parent ?: return false
return parent.hasSuppressWildcardsAnnotationInHierarchy()
}
+
+/**
+ * Returns the inner arguments for this type.
+ *
+ * Specifically it excludes outer type args when this type is an inner type.
+ *
+ * Needed due to https://github.com/google/ksp/issues/2065
+ */
+val KSType.innerArguments: List<KSTypeArgument>
+ get() =
+ if (arguments.isNotEmpty()) {
+ arguments.subList(0, declaration.typeParameters.size)
+ } else {
+ emptyList()
+ }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeJavaPoetExt.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeJavaPoetExt.kt
index 07910e7..fac410c 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeJavaPoetExt.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeJavaPoetExt.kt
@@ -21,6 +21,7 @@
import androidx.room.compiler.processing.tryBox
import androidx.room.compiler.processing.util.ISSUE_TRACKER_LINK
import com.google.devtools.ksp.KspExperimental
+import com.google.devtools.ksp.outerType
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.symbol.KSDeclaration
import com.google.devtools.ksp.symbol.KSName
@@ -158,28 +159,52 @@
resolver: Resolver,
typeResolutionContext: TypeResolutionContext,
): JTypeName {
- return if (declaration is KSTypeAlias) {
- replaceTypeAliases(resolver).asJTypeName(resolver, typeResolutionContext)
- } else if (
- this.arguments.isNotEmpty() &&
+ if (declaration is KSTypeAlias) {
+ return replaceTypeAliases(resolver).asJTypeName(resolver, typeResolutionContext)
+ }
+ val typeName = declaration.asJTypeName(resolver, typeResolutionContext)
+ val isJavaPrimitiveOrVoid = typeName.tryBox() !== typeName
+ if (
+ !isTypeParameter() &&
!resolver.isJavaRawType(this) &&
// Excluding generic value classes used directly otherwise we may generate something
// like `Object<String>`.
- !(declaration.isValueClass() && declaration.isUsedDirectly(typeResolutionContext))
+ !(declaration.isValueClass() && declaration.isUsedDirectly(typeResolutionContext)) &&
+ !isJavaPrimitiveOrVoid
) {
val args: Array<JTypeName> =
- this.arguments
+ this.innerArguments
.map { typeArg -> typeArg.asJTypeName(resolver, typeResolutionContext) }
.map { it.tryBox() }
.toTypedArray()
- when (val typeName = declaration.asJTypeName(resolver, typeResolutionContext).tryBox()) {
- is JArrayTypeName -> JArrayTypeName.of(args.single())
- is JClassName -> JParameterizedTypeName.get(typeName, *args)
+ when (typeName) {
+ is JArrayTypeName -> {
+ return if (args.isEmpty()) {
+ // e.g. IntArray
+ typeName
+ } else {
+ JArrayTypeName.of(args.single())
+ }
+ }
+ is JClassName -> {
+ val outerType = this.outerType
+ if (outerType != null) {
+ val outerTypeName = outerType.asJTypeName(resolver, typeResolutionContext)
+ if (outerTypeName is JParameterizedTypeName) {
+ return outerTypeName.nestedClass(typeName.simpleName(), args.toList())
+ }
+ }
+ return if (args.isEmpty()) {
+ typeName
+ } else {
+ JParameterizedTypeName.get(typeName, *args)
+ }
+ }
else -> error("Unexpected type name for KSType: $typeName")
}
} else {
- this.declaration.asJTypeName(resolver, typeResolutionContext)
+ return typeName
}
}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeKotlinPoetExt.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeKotlinPoetExt.kt
index c564ae1..8e16852 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeKotlinPoetExt.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeKotlinPoetExt.kt
@@ -18,6 +18,7 @@
import androidx.room.compiler.processing.util.ISSUE_TRACKER_LINK
import com.google.devtools.ksp.KspExperimental
+import com.google.devtools.ksp.outerType
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.symbol.KSDeclaration
import com.google.devtools.ksp.symbol.KSName
@@ -32,6 +33,7 @@
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.javapoet.KClassName
+import com.squareup.kotlinpoet.javapoet.KParameterizedTypeName
import com.squareup.kotlinpoet.javapoet.KTypeName
import com.squareup.kotlinpoet.javapoet.KTypeVariableName
import com.squareup.kotlinpoet.javapoet.KWildcardTypeName
@@ -128,24 +130,38 @@
resolver: Resolver,
typeArgumentTypeLookup: KTypeArgumentTypeLookup
): KTypeName {
- return if (declaration is KSTypeAlias) {
- replaceTypeAliases(resolver).asKTypeName(resolver, typeArgumentTypeLookup)
- } else
- if (this.arguments.isNotEmpty() && !resolver.isJavaRawType(this)) {
- val args: List<KTypeName> =
- this.arguments.map { typeArg ->
- typeArg.asKTypeName(
- resolver = resolver,
- typeArgumentTypeLookup = typeArgumentTypeLookup
- )
- }
- val typeName = declaration.asKTypeName(resolver, typeArgumentTypeLookup)
- check(typeName is KClassName) { "Unexpected type name for KSType: $typeName" }
- typeName.parameterizedBy(args)
- } else {
- this.declaration.asKTypeName(resolver, typeArgumentTypeLookup)
+ if (declaration is KSTypeAlias) {
+ return replaceTypeAliases(resolver).asKTypeName(resolver, typeArgumentTypeLookup)
+ }
+ fun resolveTypeName(): KTypeName {
+ val typeName = declaration.asKTypeName(resolver, typeArgumentTypeLookup)
+ if (!isTypeParameter() && !resolver.isJavaRawType(this)) {
+ check(typeName is KClassName) { "Unexpected type name for KSType: $typeName" }
+ val args: List<KTypeName> =
+ this.innerArguments.map { typeArg ->
+ typeArg.asKTypeName(
+ resolver = resolver,
+ typeArgumentTypeLookup = typeArgumentTypeLookup
+ )
+ }
+ val outerType = this.outerType
+ if (outerType != null) {
+ val outerTypeName = outerType.asKTypeName(resolver, typeArgumentTypeLookup)
+ if (outerTypeName is KParameterizedTypeName) {
+ return outerTypeName.nestedClass(typeName.simpleName, args)
+ }
}
- .copy(nullable = isMarkedNullable || nullability == Nullability.PLATFORM)
+ return if (args.isEmpty()) {
+ typeName
+ } else {
+ typeName.parameterizedBy(args)
+ }
+ } else {
+ return typeName
+ }
+ }
+ return resolveTypeName()
+ .copy(nullable = isMarkedNullable || nullability == Nullability.PLATFORM)
}
/** See [KTypeVariableNameFactory.newInstance] */
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt
index fd53c9fe..4fec882 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt
@@ -408,9 +408,9 @@
}
val arguments =
newTypeArguments
- ?: newType.arguments.indices.map { i ->
+ ?: newType.innerArguments.indices.map { i ->
KSTypeArgumentWrapper(
- originalTypeArg = newType.arguments[i],
+ originalTypeArg = newType.innerArguments[i],
typeParam = newType.declaration.typeParameters[i],
resolver = resolver,
)
@@ -451,7 +451,18 @@
fun isTypeParameter() = originalType.isTypeParameter()
- fun unwrap() = newType.replace(arguments.map { it.unwrap() })
+ fun unwrap(): KSType {
+ val newArgs = arguments.map { it.unwrap() }
+ return newType.replace(
+ newType.arguments.mapIndexed { index, oldArg ->
+ if (index < newArgs.size) {
+ newArgs[index]
+ } else {
+ oldArg
+ }
+ }
+ )
+ }
override fun toString() = buildString {
if (originalType.annotations.toList().isNotEmpty()) {
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
index 85a8062..cfbaa82 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
@@ -104,9 +104,14 @@
null
} else {
declaration.superTypes
- .singleOrNull {
- val declaration = it.resolve().declaration.replaceTypeAliases()
- declaration is KSClassDeclaration && declaration.classKind == ClassKind.CLASS
+ .firstOrNull {
+ val type = it.resolve()
+ val declaration = type.declaration.replaceTypeAliases()
+ declaration is KSClassDeclaration &&
+ (declaration.classKind == ClassKind.CLASS &&
+ // Filter out error class declarations, for consistency with KAPT these
+ // are exposed as super interfaces.
+ (isFromJava() || !type.isError))
}
?.let { env.wrap(it).makeNonNullable() } ?: anyTypeElement.type
}
@@ -115,8 +120,13 @@
override val superInterfaces by lazy {
declaration.superTypes
.filter {
- val declaration = it.resolve().declaration.replaceTypeAliases()
- declaration is KSClassDeclaration && declaration.classKind == ClassKind.INTERFACE
+ val type = it.resolve()
+ val declaration = type.declaration.replaceTypeAliases()
+ declaration is KSClassDeclaration &&
+ (declaration.classKind == ClassKind.INTERFACE ||
+ // Workaround https://github.com/google/ksp/issues/1443 by exposing
+ // error class declarations as super interfaces.
+ (isFromKotlin() && type.isError))
}
.mapTo(mutableListOf()) { env.wrap(it).makeNonNullable() }
}
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XNullabilityTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XNullabilityTest.kt
index 8c7fd8f..17ba896 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XNullabilityTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XNullabilityTest.kt
@@ -57,6 +57,9 @@
public String returnsNonNull() {
return "";
}
+ public int returnsPrimitiveInt() {
+ return 0;
+ }
public String parameters(
int primitiveParam,
@@ -76,6 +79,10 @@
element.getField("primitiveInt").let { field ->
assertThat(field.type.nullability).isEqualTo(NONNULL)
}
+ element.getMethodByJvmName("returnsPrimitiveInt").let { method ->
+ assertThat(method.returnType.nullability).isEqualTo(NONNULL)
+ assertThat(method.executableType.returnType.nullability).isEqualTo(NONNULL)
+ }
element.getField("boxedInt").let { field ->
assertThat(field.type.nullability).isEqualTo(UNKNOWN)
}
@@ -161,6 +168,8 @@
nonNullGenericWithNullableType: List<Int?>
) {
}
+
+ val nullableLambda: ((String) -> Int)? = null
}
"""
.trimIndent()
@@ -246,6 +255,9 @@
Triple("nonNullGenericWithNullableType", NONNULL, NULLABLE)
)
}
+ element.getField("nullableLambda").let { field ->
+ assertThat(field.type.nullability).isEqualTo(NULLABLE)
+ }
}
}
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt
index 66738e0..f9a7379 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt
@@ -543,24 +543,115 @@
}
@Test
- fun errorTypeForSuper() {
- val missingTypeRef =
+ fun errorTypeForSuperJava() {
+ val missingSuperClassType =
Source.java(
"foo.bar.Baz",
"""
package foo.bar;
public class Baz extends IDontExist {
- NotExistingType foo() {
- throw new RuntimeException("Stub");
- }
}
"""
.trimIndent()
)
- runProcessorTest(sources = listOf(missingTypeRef)) {
+ runProcessorTest(sources = listOf(missingSuperClassType)) {
+ it.assertCompilationResult { compilationDidFail() }
val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
assertThat(element.superClass?.isError()).isTrue()
+ assertThat(element.superInterfaces).isEmpty()
+ }
+
+ val missingSuperInterfaceType =
+ Source.java(
+ "foo.bar.Baz",
+ """
+ package foo.bar;
+ public class Baz implements IDontExist {
+ }
+ """
+ .trimIndent()
+ )
+ runProcessorTest(sources = listOf(missingSuperInterfaceType)) {
it.assertCompilationResult { compilationDidFail() }
+ val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
+ if (it.isKsp) { // Due to https://github.com/google/ksp/issues/1443
+ assertThat(element.superClass?.isError()).isTrue()
+ assertThat(element.superInterfaces).isEmpty()
+ } else {
+ assertThat(element.superClass?.isError()).isFalse()
+ assertThat(element.superInterfaces).isNotEmpty()
+ assertThat(element.superInterfaces.single().isError()).isTrue()
+ }
+ }
+ }
+
+ @Test
+ fun errorTypeForSuperKotlin() {
+ val src =
+ Source.kotlin(
+ "Subject.kt",
+ """
+ package test
+
+ interface SubjectInterface : MissingType
+ class SubjectClassOne : MissingType
+ class SubjectClassTwo : MissingType()
+ class SubjectClassThree : ValidSuperClass(), MissingType
+ class SubjectClassFour : ValidSuperInterface, MissingType
+
+ abstract class ValidSuperClass
+ interface ValidSuperInterface
+ """
+ .trimIndent()
+ )
+ runProcessorTest(
+ sources = listOf(src),
+ kotlincArguments =
+ listOf("-P", "plugin:org.jetbrains.kotlin.kapt3:correctErrorTypes=true")
+ ) { invocation ->
+ invocation.assertCompilationResult { compilationDidFail() }
+ invocation.processingEnv.requireTypeElement("test.SubjectInterface").let {
+ assertThat(it.superInterfaces).isNotEmpty()
+ assertThat(it.superInterfaces.first().isError()).isTrue()
+ assertThat(it.superClass).isNull() // interfaces has no super class
+ }
+ invocation.processingEnv.requireTypeElement("test.SubjectClassOne").let {
+ assertThat(it.superInterfaces).isNotEmpty()
+ assertThat(it.superInterfaces.first().isError()).isTrue()
+ assertThat(it.superClass).isNotNull()
+ assertThat(it.superClass!!.asTypeName()).isEqualTo(XTypeName.ANY_OBJECT)
+ }
+ invocation.processingEnv.requireTypeElement("test.SubjectClassTwo").let {
+ if (invocation.isKsp) {
+ // In KSP regardless of the parenthesis in the super type, they are always
+ // classified as class declarations.
+ assertThat(it.superInterfaces).isNotEmpty()
+ assertThat(it.superInterfaces.first().isError()).isTrue()
+ assertThat(it.superClass).isNotNull()
+ assertThat(it.superClass!!.asTypeName()).isEqualTo(XTypeName.ANY_OBJECT)
+ } else {
+ // In KAPT depending if the super type has a parenthesis or not, indicating
+ // it is a super class not a super interface, then the stub will correctly
+ // put the reference in 'extends' vs 'implements'.
+ assertThat(it.superInterfaces).isEmpty()
+ assertThat(it.superClass).isNotNull()
+ assertThat(it.superClass!!.isError()).isTrue()
+ }
+ }
+ invocation.processingEnv.requireTypeElement("test.SubjectClassThree").let {
+ assertThat(it.superInterfaces).isNotEmpty()
+ assertThat(it.superInterfaces.first().isError()).isTrue()
+ assertThat(it.superClass).isNotNull()
+ assertThat(it.superClass!!.asTypeName())
+ .isEqualTo(XClassName.get("test", "ValidSuperClass"))
+ }
+ invocation.processingEnv.requireTypeElement("test.SubjectClassFour").let {
+ assertThat(it.superInterfaces).isNotEmpty()
+ assertThat(it.superInterfaces[0].isError()).isFalse()
+ assertThat(it.superInterfaces[1].isError()).isTrue()
+ assertThat(it.superClass).isNotNull()
+ assertThat(it.superClass!!.asTypeName()).isEqualTo(XTypeName.ANY_OBJECT)
+ }
}
}
@@ -2455,4 +2546,299 @@
}
}
}
+
+ @Test
+ fun innerTypeNames(@TestParameter isPrecompiled: Boolean) {
+ val kotlinSrc =
+ Source.kotlin(
+ "KotlinOuter.kt",
+ """
+ class KotlinOuter<T> {
+ inner class Inner<P> {
+ inner class InnerAgain<Q>
+ }
+ inner class InnerWithoutArgs
+ }
+ class KotlinOuterWithoutArgs {
+ inner class Inner<P> {
+ inner class InnerAgain<Q>
+ }
+ inner class InnerWithoutArgs
+ }
+ class KotlinClient {
+ fun outer(): KotlinOuter<String> = TODO()
+ fun inner(): KotlinOuter<String>.Inner<Number> = TODO()
+ fun innerAgain(): KotlinOuter<String>.Inner<Number>.InnerAgain<Boolean> = TODO()
+ fun innerWithoutArgs(): KotlinOuter<String>.InnerWithoutArgs = TODO()
+ fun outerWithoutArgs(): KotlinOuterWithoutArgs = TODO()
+ fun innerInOuterWithoutArgs(): KotlinOuterWithoutArgs.Inner<String> = TODO()
+ fun innerAgainInOuterWithoutArgs(): KotlinOuterWithoutArgs.Inner<String>.InnerAgain<Number> = TODO()
+ fun innerWithoutArgsInOuterWithoutArgs(): KotlinOuterWithoutArgs.InnerWithoutArgs = TODO()
+ }
+ """
+ .trimIndent()
+ )
+ val javaSrc =
+ Source.java(
+ "JavaOuter",
+ """
+ class JavaOuter<T> {
+ class Nested<P> {
+ class NestedAgain<Q> {}
+ class NestedAgainWithoutArgs {}
+ }
+ class NestedWithoutArgs {}
+ }
+ class JavaOuterWithoutArgs {
+ class Nested<P> {
+ class NestedAgain<Q> {}
+ }
+ class NestedWithoutArgs {}
+ }
+ class JavaClient {
+ JavaOuter<String> javaOuter() { throw new RuntimeException("Stub"); }
+ JavaOuter<String>.Nested<Number> nested() { throw new RuntimeException("Stub"); }
+ JavaOuter<String>.Nested<Number>.NestedAgain<Boolean> nestedAgain() { throw new RuntimeException("Stub"); }
+ JavaOuter<String>.Nested<Number>.NestedAgainWithoutArgs nestedAgainWithoutArgs() { throw new RuntimeException("Stub"); }
+ JavaOuter<String>.NestedWithoutArgs nestedWithoutArgs() { throw new RuntimeException("Stub"); }
+ JavaOuter javaOuterRaw() { throw new RuntimeException("Stub"); }
+ JavaOuterWithoutArgs javaOuterWithoutArgs() { throw new RuntimeException("Stub"); }
+ JavaOuterWithoutArgs.Nested<String> nestedInOuterWithoutArgs() { throw new RuntimeException("Stub"); }
+ JavaOuterWithoutArgs.Nested<String>.NestedAgain<Boolean> nestedAgainInOuterWithoutArgs() { throw new RuntimeException("Stub"); }
+ JavaOuterWithoutArgs.NestedWithoutArgs nestedWithoutArgsInOuterWithoutArgs() { throw new RuntimeException("Stub"); }
+ }
+ """
+ .trimIndent()
+ )
+ runProcessorTest(
+ sources =
+ if (isPrecompiled) {
+ emptyList()
+ } else {
+ listOf(kotlinSrc, javaSrc)
+ },
+ classpath =
+ if (isPrecompiled) {
+ compileFiles(listOf(kotlinSrc, javaSrc))
+ } else {
+ emptyList()
+ },
+ ) { invocation ->
+ invocation.processingEnv.requireTypeElement("KotlinClient").let { cls ->
+ cls.getDeclaredMethodByJvmName("outer").returnType.asTypeName().let { typeName ->
+ assertThat(typeName.java.toString()).isEqualTo("KotlinOuter<java.lang.String>")
+ if (invocation.isKsp) {
+ assertThat(typeName.kotlin.toString())
+ .isEqualTo("KotlinOuter<kotlin.String>")
+ }
+ }
+
+ cls.getDeclaredMethodByJvmName("inner").returnType.asTypeName().let { typeName ->
+ assertThat(typeName.java.toString())
+ .isEqualTo("KotlinOuter<java.lang.String>.Inner<java.lang.Number>")
+ if (invocation.isKsp) {
+ assertThat(typeName.kotlin.toString())
+ .isEqualTo("KotlinOuter<kotlin.String>.Inner<kotlin.Number>")
+ }
+ }
+
+ cls.getDeclaredMethodByJvmName("innerAgain").returnType.asTypeName().let { typeName
+ ->
+ assertThat(typeName.java.toString())
+ .isEqualTo(
+ "KotlinOuter<java.lang.String>.Inner<java.lang.Number>.InnerAgain<java.lang.Boolean>"
+ )
+ if (invocation.isKsp) {
+ assertThat(typeName.kotlin.toString())
+ .isEqualTo(
+ "KotlinOuter<kotlin.String>.Inner<kotlin.Number>.InnerAgain<kotlin.Boolean>"
+ )
+ }
+ }
+
+ cls.getDeclaredMethodByJvmName("innerWithoutArgs").returnType.asTypeName().let {
+ typeName ->
+ assertThat(typeName.java.toString())
+ .isEqualTo("KotlinOuter<java.lang.String>.InnerWithoutArgs")
+ if (invocation.isKsp) {
+ assertThat(typeName.kotlin.toString())
+ .isEqualTo("KotlinOuter<kotlin.String>.InnerWithoutArgs")
+ }
+ }
+
+ cls.getDeclaredMethodByJvmName("outerWithoutArgs").returnType.asTypeName().let {
+ typeName ->
+ assertThat(typeName.java.toString()).isEqualTo("KotlinOuterWithoutArgs")
+ if (invocation.isKsp) {
+ assertThat(typeName.kotlin.toString()).isEqualTo("KotlinOuterWithoutArgs")
+ }
+ }
+
+ cls.getDeclaredMethodByJvmName("innerInOuterWithoutArgs")
+ .returnType
+ .asTypeName()
+ .let { typeName ->
+ assertThat(typeName.java.toString())
+ .isEqualTo("KotlinOuterWithoutArgs.Inner<java.lang.String>")
+ if (invocation.isKsp) {
+ assertThat(typeName.kotlin.toString())
+ .isEqualTo("KotlinOuterWithoutArgs.Inner<kotlin.String>")
+ }
+ }
+
+ cls.getDeclaredMethodByJvmName("innerAgainInOuterWithoutArgs")
+ .returnType
+ .asTypeName()
+ .let { typeName ->
+ assertThat(typeName.java.toString())
+ .isEqualTo(
+ "KotlinOuterWithoutArgs.Inner<java.lang.String>.InnerAgain<java.lang.Number>"
+ )
+ if (invocation.isKsp) {
+ assertThat(typeName.kotlin.toString())
+ .isEqualTo(
+ "KotlinOuterWithoutArgs.Inner<kotlin.String>.InnerAgain<kotlin.Number>"
+ )
+ }
+ }
+
+ cls.getDeclaredMethodByJvmName("innerWithoutArgsInOuterWithoutArgs")
+ .returnType
+ .asTypeName()
+ .let { typeName ->
+ assertThat(typeName.java.toString())
+ .isEqualTo("KotlinOuterWithoutArgs.InnerWithoutArgs")
+ if (invocation.isKsp) {
+ assertThat(typeName.kotlin.toString())
+ .isEqualTo("KotlinOuterWithoutArgs.InnerWithoutArgs")
+ }
+ }
+ }
+
+ invocation.processingEnv.requireTypeElement("JavaClient").let { cls ->
+ cls.getDeclaredMethodByJvmName("javaOuter").returnType.asTypeName().let { typeName
+ ->
+ assertThat(typeName.java.toString()).isEqualTo("JavaOuter<java.lang.String>")
+ if (invocation.isKsp)
+ assertThat(typeName.kotlin.toString())
+ .isEqualTo("JavaOuter<kotlin.String?>?")
+ }
+
+ if (!isPrecompiled && invocation.isKsp) {
+ // https://github.com/google/ksp/issues/2066
+ } else {
+ cls.getDeclaredMethodByJvmName("nested").returnType.asTypeName().let { typeName
+ ->
+ assertThat(typeName.java.toString())
+ .isEqualTo("JavaOuter<java.lang.String>.Nested<java.lang.Number>")
+ if (invocation.isKsp) {
+ assertThat(typeName.kotlin.toString())
+ .isEqualTo("JavaOuter<kotlin.String?>.Nested<kotlin.Number?>?")
+ }
+ }
+
+ cls.getDeclaredMethodByJvmName("nestedAgain").returnType.asTypeName().let {
+ typeName ->
+ assertThat(typeName.java.toString())
+ .isEqualTo(
+ "JavaOuter<java.lang.String>.Nested<java.lang.Number>.NestedAgain<java.lang.Boolean>"
+ )
+ if (invocation.isKsp) {
+ assertThat(typeName.kotlin.toString())
+ .isEqualTo(
+ "JavaOuter<kotlin.String?>.Nested<kotlin.Number?>.NestedAgain<kotlin.Boolean?>?"
+ )
+ }
+ }
+
+ cls.getDeclaredMethodByJvmName("nestedWithoutArgs")
+ .returnType
+ .asTypeName()
+ .let { typeName ->
+ assertThat(typeName.java.toString())
+ .isEqualTo("JavaOuter<java.lang.String>.NestedWithoutArgs")
+ if (invocation.isKsp) {
+ assertThat(typeName.kotlin.toString())
+ .isEqualTo("JavaOuter<kotlin.String?>.NestedWithoutArgs?")
+ }
+ }
+
+ cls.getDeclaredMethodByJvmName("javaOuterRaw").returnType.asTypeName().let {
+ typeName ->
+ assertThat(typeName.java.toString()).isEqualTo("JavaOuter")
+ if (invocation.isKsp) {
+ // TODO: This is wrong as Kotlin doesn't allow raw types, but it's
+ // probably not possible for us to know what type arg to fill when the\
+ // type parameter has multiple bounds.
+ assertThat(typeName.kotlin.toString()).isEqualTo("JavaOuter?")
+ }
+ }
+
+ cls.getDeclaredMethodByJvmName("javaOuterWithoutArgs")
+ .returnType
+ .asTypeName()
+ .let { typeName ->
+ assertThat(typeName.java.toString()).isEqualTo("JavaOuterWithoutArgs")
+ if (invocation.isKsp) {
+ assertThat(typeName.kotlin.toString())
+ .isEqualTo("JavaOuterWithoutArgs?")
+ }
+ }
+
+ cls.getDeclaredMethodByJvmName("nestedInOuterWithoutArgs")
+ .returnType
+ .asTypeName()
+ .let { typeName ->
+ assertThat(typeName.java.toString())
+ .isEqualTo("JavaOuterWithoutArgs.Nested<java.lang.String>")
+ if (invocation.isKsp) {
+ assertThat(typeName.kotlin.toString())
+ .isEqualTo("JavaOuterWithoutArgs.Nested<kotlin.String?>?")
+ }
+ }
+ cls.getDeclaredMethodByJvmName("nestedAgainInOuterWithoutArgs")
+ .returnType
+ .asTypeName()
+ .let { typeName ->
+ assertThat(typeName.java.toString())
+ .isEqualTo(
+ "JavaOuterWithoutArgs.Nested<java.lang.String>.NestedAgain<java.lang.Boolean>"
+ )
+ if (invocation.isKsp) {
+ assertThat(typeName.kotlin.toString())
+ .isEqualTo(
+ "JavaOuterWithoutArgs.Nested<kotlin.String?>.NestedAgain<kotlin.Boolean?>?"
+ )
+ }
+ }
+ cls.getDeclaredMethodByJvmName("nestedWithoutArgsInOuterWithoutArgs")
+ .returnType
+ .asTypeName()
+ .let { typeName ->
+ assertThat(typeName.java.toString())
+ .isEqualTo("JavaOuterWithoutArgs.NestedWithoutArgs")
+ if (invocation.isKsp) {
+ assertThat(typeName.kotlin.toString())
+ .isEqualTo("JavaOuterWithoutArgs.NestedWithoutArgs?")
+ }
+ }
+ cls.getDeclaredMethodByJvmName("nestedAgainWithoutArgs")
+ .returnType
+ .asTypeName()
+ .let { typeName ->
+ assertThat(typeName.java.toString())
+ .isEqualTo(
+ "JavaOuter<java.lang.String>.Nested<java.lang.Number>.NestedAgainWithoutArgs"
+ )
+ if (invocation.isKsp) {
+ assertThat(typeName.kotlin.toString())
+ .isEqualTo(
+ "JavaOuter<kotlin.String?>.Nested<kotlin.Number?>.NestedAgainWithoutArgs?"
+ )
+ }
+ }
+ }
+ }
+ }
+ }
}
diff --git a/room/room-compiler/build.gradle b/room/room-compiler/build.gradle
index 060f27d..8494f84 100644
--- a/room/room-compiler/build.gradle
+++ b/room/room-compiler/build.gradle
@@ -82,7 +82,7 @@
testImplementation(libs.autoValue) // to access the processor in tests
testImplementation(libs.autoServiceAnnotations)
testImplementation(libs.autoService) // to access the processor in tests
- testImplementation(projectOrArtifact(":paging:paging-common"))
+ testImplementation(project(":paging:paging-common"))
testImplementation(project(":room:room-compiler-processing-testing"))
testImplementation(libs.junit)
testImplementation(libs.jsr250)
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/RxQueryResultBinderProvider.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/RxQueryResultBinderProvider.kt
index aad54a6..ecb23d9 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/RxQueryResultBinderProvider.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/RxQueryResultBinderProvider.kt
@@ -33,7 +33,8 @@
context.processingEnv.findType(rxType.className.canonicalName)?.rawType
}
- override fun extractTypeArg(declared: XType): XType = declared.typeArguments.first()
+ override fun extractTypeArg(declared: XType): XType =
+ declared.typeArguments.first().makeNullable()
override fun create(
typeArg: XType,
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/callableQuery_rx2.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/callableQuery_rx2.kt
index 18e3a2b..1a220ab 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/callableQuery_rx2.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/callableQuery_rx2.kt
@@ -49,7 +49,7 @@
}
val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
val _cursorIndexOfOther: Int = getColumnIndexOrThrow(_stmt, "other")
- val _result: MyEntity
+ val _result: MyEntity?
if (_stmt.step()) {
val _tmpPk: Int
_tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
@@ -57,7 +57,7 @@
_tmpOther = _stmt.getText(_cursorIndexOfOther)
_result = MyEntity(_tmpPk,_tmpOther)
} else {
- error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
+ _result = null
}
_result
} finally {
@@ -87,7 +87,7 @@
}
val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
val _cursorIndexOfOther: Int = getColumnIndexOrThrow(_stmt, "other")
- val _result: MyEntity
+ val _result: MyEntity?
if (_stmt.step()) {
val _tmpPk: Int
_tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
@@ -95,7 +95,7 @@
_tmpOther = _stmt.getText(_cursorIndexOfOther)
_result = MyEntity(_tmpPk,_tmpOther)
} else {
- error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
+ _result = null
}
_result
} finally {
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/callableQuery_rx3.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/callableQuery_rx3.kt
index 463fa8b..509762a 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/callableQuery_rx3.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/callableQuery_rx3.kt
@@ -49,7 +49,7 @@
}
val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
val _cursorIndexOfOther: Int = getColumnIndexOrThrow(_stmt, "other")
- val _result: MyEntity
+ val _result: MyEntity?
if (_stmt.step()) {
val _tmpPk: Int
_tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
@@ -57,7 +57,7 @@
_tmpOther = _stmt.getText(_cursorIndexOfOther)
_result = MyEntity(_tmpPk,_tmpOther)
} else {
- error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
+ _result = null
}
_result
} finally {
@@ -87,7 +87,7 @@
}
val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
val _cursorIndexOfOther: Int = getColumnIndexOrThrow(_stmt, "other")
- val _result: MyEntity
+ val _result: MyEntity?
if (_stmt.step()) {
val _tmpPk: Int
_tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
@@ -95,7 +95,7 @@
_tmpOther = _stmt.getText(_cursorIndexOfOther)
_result = MyEntity(_tmpPk,_tmpOther)
} else {
- error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
+ _result = null
}
_result
} finally {
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomTrackingLiveData.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomTrackingLiveData.android.kt
index 5115d6f..0deb550 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomTrackingLiveData.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomTrackingLiveData.android.kt
@@ -22,8 +22,8 @@
import androidx.sqlite.SQLiteConnection
import java.util.concurrent.Callable
import java.util.concurrent.atomic.AtomicBoolean
+import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
/**
* A LiveData implementation that closely works with [InvalidationTracker] to implement database
@@ -53,6 +53,17 @@
private val computing = AtomicBoolean(false)
private val registeredObserver = AtomicBoolean(false)
+ private val launchContext =
+ if (database.inCompatibilityMode()) {
+ if (inTransaction) {
+ database.getTransactionContext()
+ } else {
+ database.getQueryContext()
+ }
+ } else {
+ EmptyCoroutineContext
+ }
+
private suspend fun refresh() {
if (registeredObserver.compareAndSet(false, true)) {
database.invalidationTracker.subscribe(
@@ -105,7 +116,7 @@
val isActive = hasActiveObservers()
if (invalid.compareAndSet(false, true)) {
if (isActive) {
- database.getCoroutineScope().launch { refresh() }
+ database.getCoroutineScope().launch(launchContext) { refresh() }
}
}
}
@@ -115,7 +126,7 @@
override fun onActive() {
super.onActive()
container.onActive(this)
- database.getCoroutineScope().launch { refresh() }
+ database.getCoroutineScope().launch(launchContext) { refresh() }
}
override fun onInactive() {
@@ -132,13 +143,7 @@
private val callableFunction: Callable<T?>
) : RoomTrackingLiveData<T>(database, container, inTransaction, tableNames) {
override suspend fun compute(): T? {
- val queryContext =
- if (inTransaction) {
- database.getTransactionContext()
- } else {
- database.getQueryContext()
- }
- return withContext(queryContext) { callableFunction.call() }
+ return callableFunction.call()
}
}
diff --git a/samples/SupportLeanbackDemos/build.gradle b/samples/SupportLeanbackDemos/build.gradle
index e9845a8..ce71e5e 100644
--- a/samples/SupportLeanbackDemos/build.gradle
+++ b/samples/SupportLeanbackDemos/build.gradle
@@ -19,9 +19,9 @@
implementation("androidx.lifecycle:lifecycle-extensions:2.2.0")
implementation(libs.constraintLayout)
- implementation(projectOrArtifact(":room:room-paging"))
- implementation(projectOrArtifact(":room:room-runtime"))
- annotationProcessor(projectOrArtifact(":room:room-compiler"))
+ implementation(project(":room:room-paging"))
+ implementation(project(":room:room-runtime"))
+ annotationProcessor(project(":room:room-compiler"))
}
android {
diff --git a/savedstate/savedstate/build.gradle b/savedstate/savedstate/build.gradle
index 7df64b7..b863381 100644
--- a/savedstate/savedstate/build.gradle
+++ b/savedstate/savedstate/build.gradle
@@ -51,7 +51,7 @@
androidInstrumentedTest {
dependsOn(jvmTest)
dependencies {
- implementation(projectOrArtifact(":lifecycle:lifecycle-runtime"))
+ implementation(project(":lifecycle:lifecycle-runtime"))
implementation(libs.testExtJunit)
implementation(libs.testCore)
implementation(libs.testRunner)
diff --git a/settings.gradle b/settings.gradle
index 988cca5..e8dfe9a 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -29,7 +29,12 @@
classpath("com.gradle:develocity-gradle-plugin:3.18")
classpath("com.gradle:common-custom-user-data-gradle-plugin:2.0.1")
classpath("androidx.build.gradle.gcpbuildcache:gcpbuildcache:1.0.0-beta10")
- classpath("com.android.settings:com.android.settings.gradle.plugin:8.7.0-alpha02")
+ def agpOverride = System.getenv("GRADLE_PLUGIN_VERSION")
+ if (agpOverride != null) {
+ classpath("com.android.settings:com.android.settings.gradle.plugin:$agpOverride")
+ } else {
+ classpath("com.android.settings:com.android.settings.gradle.plugin:8.7.0-alpha02")
+ }
}
}
@@ -429,6 +434,7 @@
includeProject(":camera:camera-feature-combination-query", [BuildType.CAMERA])
includeProject(":camera:camera-feature-combination-query-play-services", [BuildType.CAMERA])
includeProject(":camera:camera-lifecycle", [BuildType.CAMERA])
+includeProject(":camera:camera-media3-effect", [BuildType.CAMERA])
includeProject(":camera:camera-lifecycle:camera-lifecycle-samples", "camera/camera-lifecycle/samples", [BuildType.CAMERA])
includeProject(":camera:camera-mlkit-vision", [BuildType.CAMERA])
includeProject(":camera:camera-testing", [BuildType.CAMERA])
diff --git a/sharetarget/sharetarget/build.gradle b/sharetarget/sharetarget/build.gradle
index 83f12c4..eb41cbe 100644
--- a/sharetarget/sharetarget/build.gradle
+++ b/sharetarget/sharetarget/build.gradle
@@ -38,8 +38,8 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
}
androidx {
diff --git a/slice/slice-benchmark/build.gradle b/slice/slice-benchmark/build.gradle
index f895e78..02b8c23 100644
--- a/slice/slice-benchmark/build.gradle
+++ b/slice/slice-benchmark/build.gradle
@@ -41,8 +41,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
}
androidx {
diff --git a/slice/slice-core/build.gradle b/slice/slice-core/build.gradle
index 96d5715..9537bbb 100644
--- a/slice/slice-core/build.gradle
+++ b/slice/slice-core/build.gradle
@@ -37,8 +37,8 @@
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
annotationProcessor (project(":versionedparcelable:versionedparcelable-compiler"))
}
diff --git a/slice/slice-remotecallback/build.gradle b/slice/slice-remotecallback/build.gradle
index 2cbdcd7..d726d05 100644
--- a/slice/slice-remotecallback/build.gradle
+++ b/slice/slice-remotecallback/build.gradle
@@ -38,8 +38,8 @@
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestAnnotationProcessor project(":remotecallback:remotecallback-processor")
}
diff --git a/slice/slice-test/build.gradle b/slice/slice-test/build.gradle
index a485674..d918271 100644
--- a/slice/slice-test/build.gradle
+++ b/slice/slice-test/build.gradle
@@ -40,8 +40,8 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
}
androidx {
diff --git a/slice/slice-view/build.gradle b/slice/slice-view/build.gradle
index 302d901..88a74e9 100644
--- a/slice/slice-view/build.gradle
+++ b/slice/slice-view/build.gradle
@@ -43,8 +43,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore4, excludes.bytebuddy)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
+ androidTestImplementation(libs.mockitoCore4)
+ androidTestImplementation(libs.dexmakerMockito)
}
androidx {
diff --git a/startup/integration-tests/first-library/build.gradle b/startup/integration-tests/first-library/build.gradle
index ef0402b..75c7de1 100644
--- a/startup/integration-tests/first-library/build.gradle
+++ b/startup/integration-tests/first-library/build.gradle
@@ -29,8 +29,8 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.espressoCore)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has its own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has its own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
testImplementation(libs.junit)
}
diff --git a/startup/integration-tests/second-library/build.gradle b/startup/integration-tests/second-library/build.gradle
index 96216cb..c9c6ced 100644
--- a/startup/integration-tests/second-library/build.gradle
+++ b/startup/integration-tests/second-library/build.gradle
@@ -28,8 +28,8 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.espressoCore)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has its own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has its own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
testImplementation(libs.junit)
}
diff --git a/startup/startup-runtime/build.gradle b/startup/startup-runtime/build.gradle
index c0b692d..5a9bd97 100644
--- a/startup/startup-runtime/build.gradle
+++ b/startup/startup-runtime/build.gradle
@@ -49,8 +49,8 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.espressoCore)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has its own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has its own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
testImplementation(libs.junit)
}
diff --git a/swiperefreshlayout/swiperefreshlayout/build.gradle b/swiperefreshlayout/swiperefreshlayout/build.gradle
index 28bce37..5ae85eb 100644
--- a/swiperefreshlayout/swiperefreshlayout/build.gradle
+++ b/swiperefreshlayout/swiperefreshlayout/build.gradle
@@ -24,8 +24,8 @@
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
androidTestImplementation(libs.bundles.espressoContrib, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(project(":internal-testutils-espresso"))
androidTestImplementation(project(":internal-testutils-runtime"), {
exclude group: "androidx.swiperefreshlayout", module: "swiperefreshlayout"
diff --git a/testutils/testutils-lifecycle/build.gradle b/testutils/testutils-lifecycle/build.gradle
index b771af6..9d088a1 100644
--- a/testutils/testutils-lifecycle/build.gradle
+++ b/testutils/testutils-lifecycle/build.gradle
@@ -46,7 +46,7 @@
sourceSets {
commonMain {
dependencies {
- api(projectOrArtifact(":lifecycle:lifecycle-runtime"))
+ api(project(":lifecycle:lifecycle-runtime"))
api("androidx.annotation:annotation:1.8.1")
api(libs.kotlinStdlib)
diff --git a/testutils/testutils-mockito/build.gradle b/testutils/testutils-mockito/build.gradle
index c50d41e..8550ce9 100644
--- a/testutils/testutils-mockito/build.gradle
+++ b/testutils/testutils-mockito/build.gradle
@@ -30,7 +30,7 @@
}
dependencies {
- api(libs.mockitoCore, excludes.bytebuddy)
+ api(libs.mockitoCore)
implementation(libs.kotlinStdlib)
}
diff --git a/testutils/testutils-navigation/build.gradle b/testutils/testutils-navigation/build.gradle
index 773ee21..52271cd 100644
--- a/testutils/testutils-navigation/build.gradle
+++ b/testutils/testutils-navigation/build.gradle
@@ -31,9 +31,9 @@
}
dependencies {
- api(projectOrArtifact(":navigation:navigation-common"))
+ api(project(":navigation:navigation-common"))
- testImplementation(projectOrArtifact(":navigation:navigation-testing"))
+ testImplementation(project(":navigation:navigation-testing"))
testImplementation("androidx.arch.core:core-testing:2.1.0")
testImplementation(libs.junit)
testImplementation(libs.mockitoCore4)
diff --git a/transition/transition/build.gradle b/transition/transition/build.gradle
index d1de213..39fdd42 100644
--- a/transition/transition/build.gradle
+++ b/transition/transition/build.gradle
@@ -28,8 +28,8 @@
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.truth)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.opentest4j)
androidTestImplementation(project(":fragment:fragment"))
androidTestImplementation("androidx.appcompat:appcompat:1.1.0")
diff --git a/versionedparcelable/versionedparcelable/build.gradle b/versionedparcelable/versionedparcelable/build.gradle
index 4a1ab25..abf8f79 100644
--- a/versionedparcelable/versionedparcelable/build.gradle
+++ b/versionedparcelable/versionedparcelable/build.gradle
@@ -36,8 +36,8 @@
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.truth)
androidTestAnnotationProcessor project(":versionedparcelable:versionedparcelable-compiler")
}
diff --git a/viewpager/viewpager/build.gradle b/viewpager/viewpager/build.gradle
index 70f64ab..2e98fc6 100644
--- a/viewpager/viewpager/build.gradle
+++ b/viewpager/viewpager/build.gradle
@@ -22,8 +22,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation project(':internal-testutils-espresso')
}
diff --git a/viewpager2/viewpager2/build.gradle b/viewpager2/viewpager2/build.gradle
index 67c9a66..e3f2872 100644
--- a/viewpager2/viewpager2/build.gradle
+++ b/viewpager2/viewpager2/build.gradle
@@ -43,8 +43,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it's own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it's own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(project(":internal-testutils-espresso"))
androidTestImplementation(project(":internal-testutils-appcompat"), {
exclude group: "androidx.viewpager2", module: "viewpager2"
diff --git a/wear/benchmark/integration-tests/macrobenchmark-target/build.gradle b/wear/benchmark/integration-tests/macrobenchmark-target/build.gradle
index 4eacf1f..091eb148 100644
--- a/wear/benchmark/integration-tests/macrobenchmark-target/build.gradle
+++ b/wear/benchmark/integration-tests/macrobenchmark-target/build.gradle
@@ -39,7 +39,7 @@
dependencies {
implementation(libs.kotlinStdlib)
implementation(libs.constraintLayout)
- implementation projectOrArtifact(":activity:activity-ktx")
+ implementation project(":activity:activity-ktx")
implementation 'androidx.core:core-ktx'
implementation(libs.material)
implementation(project(":profileinstaller:profileinstaller"))
diff --git a/wear/compose/compose-foundation/api/current.txt b/wear/compose/compose-foundation/api/current.txt
index ea38e1a..596925c 100644
--- a/wear/compose/compose-foundation/api/current.txt
+++ b/wear/compose/compose-foundation/api/current.txt
@@ -388,8 +388,10 @@
public sealed interface LazyColumnLayoutInfo {
method public int getTotalItemsCount();
+ method public long getViewportSize();
method public java.util.List<androidx.wear.compose.foundation.lazy.LazyColumnVisibleItemInfo> getVisibleItems();
property public abstract int totalItemsCount;
+ property public abstract long viewportSize;
property public abstract java.util.List<androidx.wear.compose.foundation.lazy.LazyColumnVisibleItemInfo> visibleItems;
}
diff --git a/wear/compose/compose-foundation/api/restricted_current.txt b/wear/compose/compose-foundation/api/restricted_current.txt
index ea38e1a..596925c 100644
--- a/wear/compose/compose-foundation/api/restricted_current.txt
+++ b/wear/compose/compose-foundation/api/restricted_current.txt
@@ -388,8 +388,10 @@
public sealed interface LazyColumnLayoutInfo {
method public int getTotalItemsCount();
+ method public long getViewportSize();
method public java.util.List<androidx.wear.compose.foundation.lazy.LazyColumnVisibleItemInfo> getVisibleItems();
property public abstract int totalItemsCount;
+ property public abstract long viewportSize;
property public abstract java.util.List<androidx.wear.compose.foundation.lazy.LazyColumnVisibleItemInfo> visibleItems;
}
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfoTest.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfoTest.kt
new file mode 100644
index 0000000..f07a6b7
--- /dev/null
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfoTest.kt
@@ -0,0 +1,196 @@
+/*
+ * 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.compose.foundation.lazy
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class LazyColumnLayoutInfoTest {
+ @get:Rule val rule = createComposeRule()
+
+ private var itemSizePx: Int = 50
+ private var itemSizeDp: Dp = Dp.Infinity
+
+ @Before
+ fun before() {
+ with(rule.density) { itemSizeDp = itemSizePx.toDp() }
+ }
+
+ @Test
+ fun visibleItemsAreCorrect() {
+ lateinit var state: LazyColumnState
+
+ rule.setContent {
+ LazyColumn(
+ state = rememberLazyColumnState().also { state = it },
+ // Viewport take 4 items, item 0 is exactly above the center and there is space for
+ // two more items below the center line.
+ modifier = Modifier.requiredSize(itemSizeDp * 4f),
+ verticalArrangement = Arrangement.spacedBy(0.dp)
+ ) {
+ items((0..5).toList()) { Box(Modifier.requiredSize(itemSizeDp)) }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.viewportSize.height).isEqualTo(itemSizePx * 4)
+ // Start offset compensates for the layout where the first item is exactly above the
+ // center line.
+ state.layoutInfo.assertVisibleItems(count = 3, startOffset = itemSizePx)
+ }
+ }
+
+ @Test
+ fun visibleItemsAreCorrectWithSpacing() {
+ lateinit var state: LazyColumnState
+
+ rule.setContent {
+ LazyColumn(
+ state = rememberLazyColumnState().also { state = it },
+ // Viewport take 4 items, item 0 is exactly above the center and there is space for
+ // two more items below the center line.
+ modifier = Modifier.requiredSize(itemSizeDp * 4f),
+ verticalArrangement = Arrangement.spacedBy(itemSizeDp),
+ ) {
+ items((0..5).toList()) { Box(Modifier.requiredSize(itemSizeDp)) }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.viewportSize.height).isEqualTo(itemSizePx * 4)
+ // Start offset compensates for the layout where the first item is exactly above the
+ // center line.
+ state.layoutInfo.assertVisibleItems(
+ count = 2,
+ spacing = itemSizePx,
+ startOffset = itemSizePx
+ )
+ }
+ }
+
+ @Test
+ fun visibleItemsAreObservableWhenResize() {
+ lateinit var state: LazyColumnState
+ var size by mutableStateOf(itemSizeDp * 2)
+ var currentInfo: LazyColumnLayoutInfo? = null
+ @Composable
+ fun observingFun() {
+ currentInfo = state.layoutInfo
+ }
+ rule.setContent {
+ LazyColumn(
+ state = rememberLazyColumnState().also { state = it },
+ modifier = Modifier.requiredSize(itemSizeDp * 4f)
+ ) {
+ item { Box(Modifier.requiredSize(size)) }
+ }
+ observingFun()
+ }
+
+ rule.runOnIdle {
+ assertThat(currentInfo).isNotNull()
+ currentInfo!!.assertVisibleItems(count = 1, expectedSize = itemSizePx * 2)
+ currentInfo = null
+ size = itemSizeDp
+ }
+
+ rule.runOnIdle {
+ assertThat(currentInfo).isNotNull()
+ currentInfo!!.assertVisibleItems(count = 1, expectedSize = itemSizePx)
+ }
+ }
+
+ @Test
+ fun totalCountIsCorrect() {
+ var count by mutableStateOf(10)
+ lateinit var state: LazyColumnState
+ rule.setContent {
+ LazyColumn(state = rememberLazyColumnState().also { state = it }) {
+ items((0 until count).toList()) { Box(Modifier.requiredSize(10.dp)) }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.totalItemsCount).isEqualTo(10)
+ count = 20
+ }
+
+ rule.runOnIdle { assertThat(state.layoutInfo.totalItemsCount).isEqualTo(20) }
+ }
+
+ @Test
+ fun viewportOffsetsAndSizeAreCorrect() {
+ val sizePx = 45
+ val sizeDp = with(rule.density) { sizePx.toDp() }
+ lateinit var state: LazyColumnState
+ rule.setContent {
+ LazyColumn(
+ Modifier.height(sizeDp).width(sizeDp * 2),
+ state = rememberLazyColumnState().also { state = it }
+ ) {
+ items((0..3).toList()) { Box(Modifier.requiredSize(sizeDp)) }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.viewportSize).isEqualTo(IntSize(sizePx * 2, sizePx))
+ }
+ }
+
+ private fun LazyColumnLayoutInfo.assertVisibleItems(
+ count: Int,
+ startIndex: Int = 0,
+ startOffset: Int = 0,
+ expectedSize: Int = itemSizePx,
+ spacing: Int = 0
+ ) {
+ assertThat(visibleItems.size).isEqualTo(count)
+ var currentIndex = startIndex
+ var currentOffset = startOffset
+ visibleItems.forEach {
+ assertThat(it.index).isEqualTo(currentIndex)
+ assertWithMessage("Offset of item $currentIndex")
+ .that(it.offset)
+ .isEqualTo(currentOffset)
+ assertThat(it.height).isEqualTo(expectedSize)
+ currentIndex++
+ currentOffset += it.height + spacing
+ }
+ }
+}
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfo.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfo.kt
index c5002a0..14fc4c4 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfo.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfo.kt
@@ -16,6 +16,8 @@
package androidx.wear.compose.foundation.lazy
+import androidx.compose.ui.unit.IntSize
+
/**
* Scroll progress of an item in a [LazyColumn] before any modifications to the item's height are
* applied (using [LazyColumnItemScope.transformedHeight] modifier).
@@ -44,21 +46,26 @@
sealed interface LazyColumnVisibleItemInfo {
/** The index of the item in the underlying data source. */
val index: Int
+
/** The offset of the item from the start of the visible area. */
val offset: Int
+
/** The height of the item after applying any height changes. */
val height: Int
+
/** The scroll progress of the item, indicating its position within the visible area. */
val scrollProgress: LazyColumnItemScrollProgress
}
/** Holds the layout information for a [LazyColumn]. */
sealed interface LazyColumnLayoutInfo {
+
/** A list of [LazyColumnVisibleItemInfo] objects representing the visible items in the list. */
val visibleItems: List<LazyColumnVisibleItemInfo>
/** The total count of items passed to [LazyColumn]. */
val totalItemsCount: Int
- // TODO: b/352686661 - Expose more properties related to layout.
+ /** The size of the viewport in pixels. */
+ val viewportSize: IntSize
}
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasureResult.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasureResult.kt
index 3f316f2..1bd6f3e 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasureResult.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasureResult.kt
@@ -17,6 +17,7 @@
package androidx.wear.compose.foundation.lazy
import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.unit.IntSize
/** The result of the measure pass of the [LazyColumn]. */
internal class LazyColumnMeasureResult(
@@ -32,4 +33,8 @@
override val visibleItems: List<LazyColumnVisibleItemInfo>,
/** see [LazyColumnLayoutInfo.totalItemsCount] */
override val totalItemsCount: Int,
-) : LazyColumnLayoutInfo, MeasureResult by measureResult
+) : LazyColumnLayoutInfo, MeasureResult by measureResult {
+ /** see [LazyColumnLayoutInfo.viewportSize] */
+ override val viewportSize: IntSize
+ get() = IntSize(width = width, height = height)
+}
diff --git a/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/dialog/DialogTest.kt b/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/dialog/DialogTest.kt
index bd54d7f..9be2bf8 100644
--- a/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/dialog/DialogTest.kt
+++ b/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/dialog/DialogTest.kt
@@ -385,32 +385,6 @@
rule.onNodeWithTag(TEST_TAG).performTouchInput({ swipeRight() })
rule.onNodeWithText(dismissedText).assertExists()
}
-
- @Test
- fun calls_ondismissrequest_when_dialog_becomes_hidden() {
- val show = mutableStateOf(true)
- var dismissed = false
- rule.setContentWithTheme {
- Box {
- Dialog(
- showDialog = show.value,
- onDismissRequest = { dismissed = true },
- ) {
- Alert(
- icon = {},
- title = {},
- message = { Text("Text", modifier = Modifier.testTag(TEST_TAG)) },
- content = {},
- )
- }
- }
- }
- rule.waitForIdle()
- show.value = false
-
- rule.waitForIdle()
- assert(dismissed)
- }
}
class DialogContentSizeAndPositionTest {
diff --git a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/dialog/Dialog.android.kt b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/dialog/Dialog.android.kt
index 697583e..b07f490 100644
--- a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/dialog/Dialog.android.kt
+++ b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/dialog/Dialog.android.kt
@@ -233,17 +233,18 @@
transitionState.targetState = DialogVisibility.Hide
}
}
- }
- }
- LaunchedEffect(transitionState.currentState) {
- if (
- pendingOnDismissCall &&
- transitionState.currentState == DialogVisibility.Hide &&
- transitionState.isIdle
- ) {
- // After the outro animation, leave the dialog & reset alpha/scale transitions.
- onDismissRequest()
- pendingOnDismissCall = false
+
+ LaunchedEffect(transitionState.currentState) {
+ if (
+ pendingOnDismissCall &&
+ transitionState.currentState == DialogVisibility.Hide &&
+ transitionState.isIdle
+ ) {
+ // After the outro animation, leave the dialog & reset alpha/scale transitions.
+ onDismissRequest()
+ pendingOnDismissCall = false
+ }
+ }
}
}
}
diff --git a/wear/compose/compose-material3/api/current.txt b/wear/compose/compose-material3/api/current.txt
index cd6f675..7892df7 100644
--- a/wear/compose/compose-material3/api/current.txt
+++ b/wear/compose/compose-material3/api/current.txt
@@ -71,8 +71,12 @@
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors filledTonalButtonColors(optional long containerColor, optional long contentColor, optional long secondaryContentColor, optional long iconColor, optional long disabledContainerColor, optional long disabledContentColor, optional long disabledSecondaryContentColor, optional long disabledIconColor);
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors filledVariantButtonColors();
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors filledVariantButtonColors(optional long containerColor, optional long contentColor, optional long secondaryContentColor, optional long iconColor, optional long disabledContainerColor, optional long disabledContentColor, optional long disabledSecondaryContentColor, optional long disabledIconColor);
+ method public float getButtonExtraLargeIconStartPadding();
method public float getButtonHorizontalPadding();
+ method public float getButtonLargeIconStartPadding();
method public float getButtonVerticalPadding();
+ method public androidx.compose.foundation.layout.PaddingValues getButtonWithExtraLargeIconContentPadding();
+ method public androidx.compose.foundation.layout.PaddingValues getButtonWithLargeIconContentPadding();
method public androidx.compose.foundation.layout.PaddingValues getCompactButtonContentPadding();
method public float getCompactButtonHeight();
method public float getCompactButtonHorizontalPadding();
@@ -84,6 +88,7 @@
method public float getEdgeButtonHeightLarge();
method public float getEdgeButtonHeightMedium();
method public float getEdgeButtonHeightSmall();
+ method public float getExtraLargeIconSize();
method public float getHeight();
method public float getIconSize();
method public float getLargeIconSize();
@@ -93,8 +98,12 @@
method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke outlinedButtonBorder(boolean enabled, optional long borderColor, optional long disabledBorderColor, optional float borderWidth);
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors outlinedButtonColors();
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors outlinedButtonColors(optional long contentColor, optional long secondaryContentColor, optional long iconColor, optional long disabledContentColor, optional long disabledSecondaryContentColor, optional long disabledIconColor);
+ property public final float ButtonExtraLargeIconStartPadding;
property public final float ButtonHorizontalPadding;
+ property public final float ButtonLargeIconStartPadding;
property public final float ButtonVerticalPadding;
+ property public final androidx.compose.foundation.layout.PaddingValues ButtonWithExtraLargeIconContentPadding;
+ property public final androidx.compose.foundation.layout.PaddingValues ButtonWithLargeIconContentPadding;
property public final androidx.compose.foundation.layout.PaddingValues CompactButtonContentPadding;
property public final float CompactButtonHeight;
property public final float CompactButtonHorizontalPadding;
@@ -105,6 +114,7 @@
property public final float EdgeButtonHeightLarge;
property public final float EdgeButtonHeightMedium;
property public final float EdgeButtonHeightSmall;
+ property public final float ExtraLargeIconSize;
property public final float Height;
property public final float IconSize;
property public final float LargeIconSize;
@@ -1266,17 +1276,32 @@
method @androidx.compose.runtime.Composable public static void TextToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.wear.compose.material3.TextToggleButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.foundation.BorderStroke? border, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
}
+ public final class TextConfiguration {
+ ctor public TextConfiguration(androidx.compose.ui.text.style.TextAlign? textAlign, int overflow, int maxLines);
+ method public int getMaxLines();
+ method public int getOverflow();
+ method public androidx.compose.ui.text.style.TextAlign? getTextAlign();
+ property public final int maxLines;
+ property public final int overflow;
+ property public final androidx.compose.ui.text.style.TextAlign? textAlign;
+ }
+
+ public final class TextConfigurationDefaults {
+ method public int getOverflow();
+ method public androidx.compose.ui.text.style.TextAlign? getTextAlign();
+ property public final int Overflow;
+ property public final androidx.compose.ui.text.style.TextAlign? TextAlign;
+ field public static final androidx.wear.compose.material3.TextConfigurationDefaults INSTANCE;
+ field public static final int MaxLines = 2147483647; // 0x7fffffff
+ }
+
public final class TextKt {
method @androidx.compose.runtime.Composable public static void ProvideTextStyle(androidx.compose.ui.text.TextStyle value, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void Text(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
method @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
- method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.style.TextAlign?> getLocalTextAlign();
- method public static androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Integer> getLocalTextMaxLines();
- method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.style.TextOverflow> getLocalTextOverflow();
+ method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.wear.compose.material3.TextConfiguration> getLocalTextConfiguration();
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> getLocalTextStyle();
- property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.style.TextAlign?> LocalTextAlign;
- property public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Integer> LocalTextMaxLines;
- property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.style.TextOverflow> LocalTextOverflow;
+ property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.wear.compose.material3.TextConfiguration> LocalTextConfiguration;
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> LocalTextStyle;
}
@@ -1379,8 +1404,9 @@
}
@androidx.compose.runtime.Immutable public final class Typography {
- ctor public Typography(optional androidx.compose.ui.text.font.FontFamily defaultFontFamily, optional androidx.compose.ui.text.TextStyle arcMedium, optional androidx.compose.ui.text.TextStyle arcSmall, optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle bodyExtraSmall, optional androidx.compose.ui.text.TextStyle numeralExtraLarge, optional androidx.compose.ui.text.TextStyle numeralLarge, optional androidx.compose.ui.text.TextStyle numeralMedium, optional androidx.compose.ui.text.TextStyle numeralSmall, optional androidx.compose.ui.text.TextStyle numeralExtraSmall);
- method public androidx.wear.compose.material3.Typography copy(optional androidx.compose.ui.text.TextStyle arcMedium, optional androidx.compose.ui.text.TextStyle arcSmall, optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle bodyExtraSmall, optional androidx.compose.ui.text.TextStyle numeralExtraLarge, optional androidx.compose.ui.text.TextStyle numeralLarge, optional androidx.compose.ui.text.TextStyle numeralMedium, optional androidx.compose.ui.text.TextStyle numeralSmall, optional androidx.compose.ui.text.TextStyle numeralExtraSmall);
+ ctor public Typography(optional androidx.compose.ui.text.font.FontFamily defaultFontFamily, optional androidx.compose.ui.text.TextStyle arcLarge, optional androidx.compose.ui.text.TextStyle arcMedium, optional androidx.compose.ui.text.TextStyle arcSmall, optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle bodyExtraSmall, optional androidx.compose.ui.text.TextStyle numeralExtraLarge, optional androidx.compose.ui.text.TextStyle numeralLarge, optional androidx.compose.ui.text.TextStyle numeralMedium, optional androidx.compose.ui.text.TextStyle numeralSmall, optional androidx.compose.ui.text.TextStyle numeralExtraSmall);
+ method public androidx.wear.compose.material3.Typography copy(optional androidx.compose.ui.text.TextStyle arcLarge, optional androidx.compose.ui.text.TextStyle arcMedium, optional androidx.compose.ui.text.TextStyle arcSmall, optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle bodyExtraSmall, optional androidx.compose.ui.text.TextStyle numeralExtraLarge, optional androidx.compose.ui.text.TextStyle numeralLarge, optional androidx.compose.ui.text.TextStyle numeralMedium, optional androidx.compose.ui.text.TextStyle numeralSmall, optional androidx.compose.ui.text.TextStyle numeralExtraSmall);
+ method public androidx.compose.ui.text.TextStyle getArcLarge();
method public androidx.compose.ui.text.TextStyle getArcMedium();
method public androidx.compose.ui.text.TextStyle getArcSmall();
method public androidx.compose.ui.text.TextStyle getBodyExtraSmall();
@@ -1401,6 +1427,7 @@
method public androidx.compose.ui.text.TextStyle getTitleLarge();
method public androidx.compose.ui.text.TextStyle getTitleMedium();
method public androidx.compose.ui.text.TextStyle getTitleSmall();
+ property public final androidx.compose.ui.text.TextStyle arcLarge;
property public final androidx.compose.ui.text.TextStyle arcMedium;
property public final androidx.compose.ui.text.TextStyle arcSmall;
property public final androidx.compose.ui.text.TextStyle bodyExtraSmall;
diff --git a/wear/compose/compose-material3/api/restricted_current.txt b/wear/compose/compose-material3/api/restricted_current.txt
index cd6f675..7892df7 100644
--- a/wear/compose/compose-material3/api/restricted_current.txt
+++ b/wear/compose/compose-material3/api/restricted_current.txt
@@ -71,8 +71,12 @@
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors filledTonalButtonColors(optional long containerColor, optional long contentColor, optional long secondaryContentColor, optional long iconColor, optional long disabledContainerColor, optional long disabledContentColor, optional long disabledSecondaryContentColor, optional long disabledIconColor);
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors filledVariantButtonColors();
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors filledVariantButtonColors(optional long containerColor, optional long contentColor, optional long secondaryContentColor, optional long iconColor, optional long disabledContainerColor, optional long disabledContentColor, optional long disabledSecondaryContentColor, optional long disabledIconColor);
+ method public float getButtonExtraLargeIconStartPadding();
method public float getButtonHorizontalPadding();
+ method public float getButtonLargeIconStartPadding();
method public float getButtonVerticalPadding();
+ method public androidx.compose.foundation.layout.PaddingValues getButtonWithExtraLargeIconContentPadding();
+ method public androidx.compose.foundation.layout.PaddingValues getButtonWithLargeIconContentPadding();
method public androidx.compose.foundation.layout.PaddingValues getCompactButtonContentPadding();
method public float getCompactButtonHeight();
method public float getCompactButtonHorizontalPadding();
@@ -84,6 +88,7 @@
method public float getEdgeButtonHeightLarge();
method public float getEdgeButtonHeightMedium();
method public float getEdgeButtonHeightSmall();
+ method public float getExtraLargeIconSize();
method public float getHeight();
method public float getIconSize();
method public float getLargeIconSize();
@@ -93,8 +98,12 @@
method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke outlinedButtonBorder(boolean enabled, optional long borderColor, optional long disabledBorderColor, optional float borderWidth);
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors outlinedButtonColors();
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors outlinedButtonColors(optional long contentColor, optional long secondaryContentColor, optional long iconColor, optional long disabledContentColor, optional long disabledSecondaryContentColor, optional long disabledIconColor);
+ property public final float ButtonExtraLargeIconStartPadding;
property public final float ButtonHorizontalPadding;
+ property public final float ButtonLargeIconStartPadding;
property public final float ButtonVerticalPadding;
+ property public final androidx.compose.foundation.layout.PaddingValues ButtonWithExtraLargeIconContentPadding;
+ property public final androidx.compose.foundation.layout.PaddingValues ButtonWithLargeIconContentPadding;
property public final androidx.compose.foundation.layout.PaddingValues CompactButtonContentPadding;
property public final float CompactButtonHeight;
property public final float CompactButtonHorizontalPadding;
@@ -105,6 +114,7 @@
property public final float EdgeButtonHeightLarge;
property public final float EdgeButtonHeightMedium;
property public final float EdgeButtonHeightSmall;
+ property public final float ExtraLargeIconSize;
property public final float Height;
property public final float IconSize;
property public final float LargeIconSize;
@@ -1266,17 +1276,32 @@
method @androidx.compose.runtime.Composable public static void TextToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.wear.compose.material3.TextToggleButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.foundation.BorderStroke? border, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
}
+ public final class TextConfiguration {
+ ctor public TextConfiguration(androidx.compose.ui.text.style.TextAlign? textAlign, int overflow, int maxLines);
+ method public int getMaxLines();
+ method public int getOverflow();
+ method public androidx.compose.ui.text.style.TextAlign? getTextAlign();
+ property public final int maxLines;
+ property public final int overflow;
+ property public final androidx.compose.ui.text.style.TextAlign? textAlign;
+ }
+
+ public final class TextConfigurationDefaults {
+ method public int getOverflow();
+ method public androidx.compose.ui.text.style.TextAlign? getTextAlign();
+ property public final int Overflow;
+ property public final androidx.compose.ui.text.style.TextAlign? TextAlign;
+ field public static final androidx.wear.compose.material3.TextConfigurationDefaults INSTANCE;
+ field public static final int MaxLines = 2147483647; // 0x7fffffff
+ }
+
public final class TextKt {
method @androidx.compose.runtime.Composable public static void ProvideTextStyle(androidx.compose.ui.text.TextStyle value, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void Text(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
method @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
- method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.style.TextAlign?> getLocalTextAlign();
- method public static androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Integer> getLocalTextMaxLines();
- method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.style.TextOverflow> getLocalTextOverflow();
+ method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.wear.compose.material3.TextConfiguration> getLocalTextConfiguration();
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> getLocalTextStyle();
- property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.style.TextAlign?> LocalTextAlign;
- property public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Integer> LocalTextMaxLines;
- property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.style.TextOverflow> LocalTextOverflow;
+ property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.wear.compose.material3.TextConfiguration> LocalTextConfiguration;
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> LocalTextStyle;
}
@@ -1379,8 +1404,9 @@
}
@androidx.compose.runtime.Immutable public final class Typography {
- ctor public Typography(optional androidx.compose.ui.text.font.FontFamily defaultFontFamily, optional androidx.compose.ui.text.TextStyle arcMedium, optional androidx.compose.ui.text.TextStyle arcSmall, optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle bodyExtraSmall, optional androidx.compose.ui.text.TextStyle numeralExtraLarge, optional androidx.compose.ui.text.TextStyle numeralLarge, optional androidx.compose.ui.text.TextStyle numeralMedium, optional androidx.compose.ui.text.TextStyle numeralSmall, optional androidx.compose.ui.text.TextStyle numeralExtraSmall);
- method public androidx.wear.compose.material3.Typography copy(optional androidx.compose.ui.text.TextStyle arcMedium, optional androidx.compose.ui.text.TextStyle arcSmall, optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle bodyExtraSmall, optional androidx.compose.ui.text.TextStyle numeralExtraLarge, optional androidx.compose.ui.text.TextStyle numeralLarge, optional androidx.compose.ui.text.TextStyle numeralMedium, optional androidx.compose.ui.text.TextStyle numeralSmall, optional androidx.compose.ui.text.TextStyle numeralExtraSmall);
+ ctor public Typography(optional androidx.compose.ui.text.font.FontFamily defaultFontFamily, optional androidx.compose.ui.text.TextStyle arcLarge, optional androidx.compose.ui.text.TextStyle arcMedium, optional androidx.compose.ui.text.TextStyle arcSmall, optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle bodyExtraSmall, optional androidx.compose.ui.text.TextStyle numeralExtraLarge, optional androidx.compose.ui.text.TextStyle numeralLarge, optional androidx.compose.ui.text.TextStyle numeralMedium, optional androidx.compose.ui.text.TextStyle numeralSmall, optional androidx.compose.ui.text.TextStyle numeralExtraSmall);
+ method public androidx.wear.compose.material3.Typography copy(optional androidx.compose.ui.text.TextStyle arcLarge, optional androidx.compose.ui.text.TextStyle arcMedium, optional androidx.compose.ui.text.TextStyle arcSmall, optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle bodyExtraSmall, optional androidx.compose.ui.text.TextStyle numeralExtraLarge, optional androidx.compose.ui.text.TextStyle numeralLarge, optional androidx.compose.ui.text.TextStyle numeralMedium, optional androidx.compose.ui.text.TextStyle numeralSmall, optional androidx.compose.ui.text.TextStyle numeralExtraSmall);
+ method public androidx.compose.ui.text.TextStyle getArcLarge();
method public androidx.compose.ui.text.TextStyle getArcMedium();
method public androidx.compose.ui.text.TextStyle getArcSmall();
method public androidx.compose.ui.text.TextStyle getBodyExtraSmall();
@@ -1401,6 +1427,7 @@
method public androidx.compose.ui.text.TextStyle getTitleLarge();
method public androidx.compose.ui.text.TextStyle getTitleMedium();
method public androidx.compose.ui.text.TextStyle getTitleSmall();
+ property public final androidx.compose.ui.text.TextStyle arcLarge;
property public final androidx.compose.ui.text.TextStyle arcMedium;
property public final androidx.compose.ui.text.TextStyle arcSmall;
property public final androidx.compose.ui.text.TextStyle bodyExtraSmall;
diff --git a/wear/compose/compose-material3/benchmark/src/androidTest/java/androidx/wear/compose/material3/benchmark/ProgressIndicatorBenchmark.kt b/wear/compose/compose-material3/benchmark/src/androidTest/java/androidx/wear/compose/material3/benchmark/ProgressIndicatorBenchmark.kt
new file mode 100644
index 0000000..e9e624a
--- /dev/null
+++ b/wear/compose/compose-material3/benchmark/src/androidTest/java/androidx/wear/compose/material3/benchmark/ProgressIndicatorBenchmark.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.compose.material3.benchmark
+
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.benchmarkToFirstPixel
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.wear.compose.material3.CircularProgressIndicator
+import androidx.wear.compose.material3.MaterialTheme
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class ProgressIndicatorBenchmark {
+ @get:Rule val benchmarkRule = ComposeBenchmarkRule()
+
+ private val testCaseFactory = { ProgressIndicatorTestCase() }
+
+ @Test
+ fun first_pixel() {
+ benchmarkRule.benchmarkToFirstPixel(testCaseFactory)
+ }
+}
+
+internal class ProgressIndicatorTestCase : LayeredComposeTestCase() {
+ @Composable
+ override fun MeasuredContent() {
+ CircularProgressIndicator(progress = { 0.5f })
+ }
+
+ @Composable
+ override fun ContentWrappers(content: @Composable () -> Unit) {
+ MaterialTheme { content() }
+ }
+}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ButtonDemo.kt
index 1f5758e..e5e8942 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ButtonDemo.kt
@@ -19,11 +19,9 @@
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Favorite
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
@@ -36,32 +34,67 @@
import androidx.wear.compose.material3.ChildButton
import androidx.wear.compose.material3.CompactButton
import androidx.wear.compose.material3.FilledTonalButton
-import androidx.wear.compose.material3.Icon
import androidx.wear.compose.material3.ListHeader
+import androidx.wear.compose.material3.ListSubheader
import androidx.wear.compose.material3.OutlinedButton
import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.samples.ButtonExtraLargeIconSample
+import androidx.wear.compose.material3.samples.ButtonLargeIconSample
import androidx.wear.compose.material3.samples.ButtonSample
-import androidx.wear.compose.material3.samples.ButtonWithOnLongClickSample
import androidx.wear.compose.material3.samples.ChildButtonSample
-import androidx.wear.compose.material3.samples.ChildButtonWithOnLongClickSample
import androidx.wear.compose.material3.samples.CompactButtonSample
import androidx.wear.compose.material3.samples.CompactButtonWithOnLongClickSample
import androidx.wear.compose.material3.samples.FilledTonalButtonSample
-import androidx.wear.compose.material3.samples.FilledTonalButtonWithOnLongClickSample
import androidx.wear.compose.material3.samples.FilledVariantButtonSample
import androidx.wear.compose.material3.samples.OutlinedButtonSample
-import androidx.wear.compose.material3.samples.OutlinedButtonWithOnLongClickSample
import androidx.wear.compose.material3.samples.OutlinedCompactButtonSample
import androidx.wear.compose.material3.samples.SimpleChildButtonSample
import androidx.wear.compose.material3.samples.SimpleFilledTonalButtonSample
import androidx.wear.compose.material3.samples.SimpleFilledVariantButtonSample
import androidx.wear.compose.material3.samples.SimpleOutlinedButtonSample
+import androidx.wear.compose.material3.samples.icons.FavoriteIcon
+
+@Composable
+fun BaseButtonDemo() {
+ // This demo shows how to use the Base Button overload, which has a single content slot
+ // that can be used with a trailing lambda. It should vertically center content by default,
+ // but that can easily be changed by using Modifier.align from RowScope in whatever is passed
+ // to the content slot.
+ ScalingLazyDemo {
+ item { ListHeader { Text("Base Button") } }
+ item { ListSubheader { Text("Default alignment") } }
+ item { Button(onClick = {}, modifier = Modifier.fillMaxWidth()) { Text("Base Button") } }
+ item { ListSubheader { Text("Top Alignment") } }
+ item {
+ Button(onClick = {}, modifier = Modifier.fillMaxWidth()) {
+ Text("Base Button", modifier = Modifier.align(Alignment.Top))
+ }
+ }
+ }
+}
@Composable
fun ButtonDemo() {
val context = LocalContext.current
ScalingLazyDemo {
- item { ListHeader { Text("1 slot button") } }
+ item { ListHeader { Text("1 Slot Button") } }
+ item {
+ Button(
+ onClick = { /* Do something */ },
+ label = { Text("Filled Button") },
+ enabled = true,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ item {
+ Button(
+ onClick = { /* Do something */ },
+ label = { Text("Filled Button") },
+ enabled = false,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ item { ListHeader { Text("Centered Button") } }
item {
Button(
onClick = {},
@@ -89,30 +122,65 @@
modifier = Modifier.fillMaxWidth()
)
}
- item { ListHeader { Text("3 slot button") } }
+ item { ListHeader { Text("2 Slot Button") } }
+ item {
+ Button(
+ onClick = { /* Do something */ },
+ label = { Text("Button") },
+ secondaryLabel = { Text("Secondary label") },
+ enabled = true,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ item {
+ Button(
+ onClick = { /* Do something */ },
+ label = { Text("Button") },
+ secondaryLabel = { Text("Secondary label") },
+ enabled = false,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ item { ListHeader { Text("Icon and Label") } }
+ item {
+ Button(
+ onClick = { /* Do something */ },
+ label = { Text("Button") },
+ icon = { FavoriteIcon(ButtonDefaults.IconSize) },
+ enabled = true,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ item {
+ Button(
+ onClick = { /* Do something */ },
+ label = { Text("Button") },
+ icon = { FavoriteIcon(ButtonDefaults.IconSize) },
+ enabled = false,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ item { ListHeader { Text("3 Slot Button") } }
item { ButtonSample(modifier = Modifier.fillMaxWidth()) }
item {
Button(
onClick = { /* Do something */ },
label = { Text("Button") },
secondaryLabel = { Text("Secondary label") },
- icon = {
- Icon(
- Icons.Filled.Favorite,
- contentDescription = "Favorite icon",
- modifier = Modifier.size(ButtonDefaults.IconSize)
- )
- },
+ icon = { FavoriteIcon(ButtonDefaults.IconSize) },
enabled = false,
modifier = Modifier.fillMaxWidth()
)
}
item { ListHeader { Text("Long Click") } }
item {
- ButtonWithOnLongClickSample(
+ Button(
+ onClick = { showOnClickToast(context) },
+ onLongClick = { showOnLongClickToast(context) },
+ onLongClickLabel = "Long click",
+ label = { Text("Button") },
+ secondaryLabel = { Text("with long click") },
modifier = Modifier.fillMaxWidth(),
- onClickHandler = { showOnClickToast(context) },
- onLongClickHandler = { showOnLongClickToast(context) },
)
}
}
@@ -122,15 +190,9 @@
fun FilledTonalButtonDemo() {
val context = LocalContext.current
ScalingLazyDemo {
- item { ListHeader { Text("1 slot button") } }
+ item { ListHeader { Text("1 Slot Button") } }
item { SimpleFilledTonalButtonSample() }
item {
- FilledTonalButtonWithOnLongClickSample(
- onClickHandler = { showOnClickToast(context) },
- onLongClickHandler = { showOnLongClickToast(context) }
- )
- }
- item {
FilledTonalButton(
onClick = { /* Do something */ },
label = { Text("Filled Tonal Button") },
@@ -138,31 +200,73 @@
modifier = Modifier.fillMaxWidth(),
)
}
- item { ListHeader { Text("3 slot button") } }
+ item { ListHeader { Text("2 Slot Button") } }
+ item {
+ FilledTonalButton(
+ onClick = { /* Do something */ },
+ label = { Text("Filled Tonal Button") },
+ secondaryLabel = { Text("Secondary label") },
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ item {
+ FilledTonalButton(
+ onClick = { /* Do something */ },
+ label = { Text("Filled Tonal Button") },
+ secondaryLabel = { Text("Secondary label") },
+ enabled = false,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ item { ListHeader { Text("Icon and Label") } }
+ item {
+ FilledTonalButton(
+ onClick = { /* Do something */ },
+ label = { Text("Filled Tonal Button") },
+ icon = { FavoriteIcon(ButtonDefaults.IconSize) },
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ item {
+ FilledTonalButton(
+ onClick = { /* Do something */ },
+ label = { Text("Filled Tonal Button") },
+ icon = { FavoriteIcon(ButtonDefaults.IconSize) },
+ modifier = Modifier.fillMaxWidth(),
+ enabled = false
+ )
+ }
+ item { ListHeader { Text("3 Slot Button") } }
item { FilledTonalButtonSample() }
item {
FilledTonalButton(
onClick = { /* Do something */ },
label = { Text("Filled Tonal Button") },
secondaryLabel = { Text("Secondary label") },
- icon = {
- Icon(
- Icons.Filled.Favorite,
- contentDescription = "Favorite icon",
- modifier = Modifier.size(ButtonDefaults.IconSize)
- )
- },
+ icon = { FavoriteIcon(ButtonDefaults.IconSize) },
enabled = false,
modifier = Modifier.fillMaxWidth(),
)
}
+ item { ListHeader { Text("Long Click") } }
+ item {
+ FilledTonalButton(
+ onClick = { showOnClickToast(context) },
+ onLongClick = { showOnLongClickToast(context) },
+ onLongClickLabel = "Long click",
+ label = { Text("Filled Tonal Button") },
+ secondaryLabel = { Text("with long click") },
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
}
}
@Composable
fun FilledVariantButtonDemo() {
+ val context = LocalContext.current
ScalingLazyDemo {
- item { ListHeader { Text("1 slot button") } }
+ item { ListHeader { Text("1 Slot Button") } }
item { SimpleFilledVariantButtonSample() }
item {
Button(
@@ -173,7 +277,47 @@
modifier = Modifier.fillMaxWidth()
)
}
- item { ListHeader { Text("3 slot button") } }
+ item { ListHeader { Text("2 Slot Button") } }
+ item {
+ Button(
+ onClick = { /* Do something */ },
+ colors = ButtonDefaults.filledVariantButtonColors(),
+ label = { Text("Filled Variant Button") },
+ secondaryLabel = { Text("Secondary label") },
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ item {
+ Button(
+ onClick = { /* Do something */ },
+ colors = ButtonDefaults.filledVariantButtonColors(),
+ label = { Text("Filled Variant Button") },
+ secondaryLabel = { Text("Secondary label") },
+ enabled = false,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ item { ListHeader { Text("Icon and Label") } }
+ item {
+ Button(
+ onClick = { /* Do something */ },
+ colors = ButtonDefaults.filledVariantButtonColors(),
+ label = { Text("Filled Variant Button") },
+ icon = { FavoriteIcon(ButtonDefaults.IconSize) },
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ item {
+ Button(
+ onClick = { /* Do something */ },
+ colors = ButtonDefaults.filledVariantButtonColors(),
+ label = { Text("Filled Variant Button") },
+ icon = { FavoriteIcon(ButtonDefaults.IconSize) },
+ enabled = false,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ item { ListHeader { Text("3 Slot Button") } }
item { FilledVariantButtonSample() }
item {
Button(
@@ -181,15 +325,21 @@
colors = ButtonDefaults.filledVariantButtonColors(),
label = { Text("Filled Variant Button") },
secondaryLabel = { Text("Secondary label") },
- icon = {
- Icon(
- Icons.Filled.Favorite,
- contentDescription = "Favorite icon",
- modifier = Modifier.size(ButtonDefaults.IconSize)
- )
- },
+ icon = { FavoriteIcon(ButtonDefaults.IconSize) },
enabled = false,
- modifier = Modifier.fillMaxWidth()
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ item { ListHeader { Text("Long Click") } }
+ item {
+ Button(
+ onClick = { showOnClickToast(context) },
+ onLongClick = { showOnLongClickToast(context) },
+ onLongClickLabel = "Long click",
+ colors = ButtonDefaults.filledVariantButtonColors(),
+ label = { Text("Filled VariantButton") },
+ secondaryLabel = { Text("with long click") },
+ modifier = Modifier.fillMaxWidth(),
)
}
}
@@ -199,15 +349,9 @@
fun OutlinedButtonDemo() {
val context = LocalContext.current
ScalingLazyDemo {
- item { ListHeader { Text("1 slot button") } }
+ item { ListHeader { Text("1 Slot Button") } }
item { SimpleOutlinedButtonSample() }
item {
- OutlinedButtonWithOnLongClickSample(
- onClickHandler = { showOnClickToast(context) },
- onLongClickHandler = { showOnLongClickToast(context) }
- )
- }
- item {
OutlinedButton(
onClick = { /* Do something */ },
label = { Text("Outlined Button") },
@@ -215,24 +359,65 @@
modifier = Modifier.fillMaxWidth(),
)
}
- item { ListHeader { Text("3 slot button") } }
+ item { ListHeader { Text("2 Slot Button") } }
+ item {
+ OutlinedButton(
+ onClick = { /* Do something */ },
+ label = { Text("Outlined Button") },
+ secondaryLabel = { Text("Secondary label") },
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ item {
+ OutlinedButton(
+ onClick = { /* Do something */ },
+ label = { Text("Outlined Button") },
+ secondaryLabel = { Text("Secondary label") },
+ enabled = false,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ item { ListHeader { Text("Icon and Label") } }
+ item {
+ OutlinedButton(
+ onClick = { /* Do something */ },
+ label = { Text("Outlined Button") },
+ icon = { FavoriteIcon(ButtonDefaults.IconSize) },
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ item {
+ OutlinedButton(
+ onClick = { /* Do something */ },
+ label = { Text("Outlined Button") },
+ icon = { FavoriteIcon(ButtonDefaults.IconSize) },
+ enabled = false,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ item { ListHeader { Text("3 Slot Button)") } }
item { OutlinedButtonSample() }
item {
OutlinedButton(
onClick = { /* Do something */ },
label = { Text("Outlined Button") },
secondaryLabel = { Text("Secondary label") },
- icon = {
- Icon(
- Icons.Filled.Favorite,
- contentDescription = "Favorite icon",
- modifier = Modifier.size(ButtonDefaults.IconSize)
- )
- },
+ icon = { FavoriteIcon(ButtonDefaults.IconSize) },
enabled = false,
modifier = Modifier.fillMaxWidth()
)
}
+ item { ListHeader { Text("Long Click") } }
+ item {
+ OutlinedButton(
+ onClick = { showOnClickToast(context) },
+ onLongClick = { showOnLongClickToast(context) },
+ onLongClickLabel = "Long click",
+ label = { Text("Outlined Button") },
+ secondaryLabel = { Text("with long click") },
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
}
}
@@ -240,15 +425,9 @@
fun ChildButtonDemo() {
val context = LocalContext.current
ScalingLazyDemo {
- item { ListHeader { Text("1 slot button") } }
+ item { ListHeader { Text("1 Slot Button") } }
item { SimpleChildButtonSample() }
item {
- ChildButtonWithOnLongClickSample(
- onClickHandler = { showOnClickToast(context) },
- onLongClickHandler = { showOnLongClickToast(context) },
- )
- }
- item {
ChildButton(
onClick = { /* Do something */ },
label = { Text("Child Button") },
@@ -256,24 +435,65 @@
modifier = Modifier.fillMaxWidth(),
)
}
- item { ListHeader { Text("3 slot button") } }
+ item { ListHeader { Text("2 Slot Button") } }
+ item {
+ ChildButton(
+ onClick = { /* Do something */ },
+ label = { Text("Child Button") },
+ secondaryLabel = { Text("Secondary label") },
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ item {
+ ChildButton(
+ onClick = { /* Do something */ },
+ label = { Text("Child Button") },
+ secondaryLabel = { Text("Secondary label") },
+ enabled = false,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ item { ListHeader { Text("Icon and Label") } }
+ item {
+ ChildButton(
+ onClick = { /* Do something */ },
+ label = { Text("Child Button") },
+ icon = { FavoriteIcon(ButtonDefaults.IconSize) },
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ item {
+ ChildButton(
+ onClick = { /* Do something */ },
+ label = { Text("Child Button") },
+ icon = { FavoriteIcon(ButtonDefaults.IconSize) },
+ enabled = false,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ item { ListHeader { Text("3 Slot Button") } }
item { ChildButtonSample() }
item {
ChildButton(
onClick = { /* Do something */ },
label = { Text("Child Button") },
secondaryLabel = { Text("Secondary label") },
- icon = {
- Icon(
- Icons.Filled.Favorite,
- contentDescription = "Favorite icon",
- modifier = Modifier.size(ButtonDefaults.IconSize)
- )
- },
+ icon = { FavoriteIcon(ButtonDefaults.IconSize) },
enabled = false,
modifier = Modifier.fillMaxWidth()
)
}
+ item { ListHeader { Text("Long Click") } }
+ item {
+ ChildButton(
+ onClick = { showOnClickToast(context) },
+ onLongClick = { showOnLongClickToast(context) },
+ onLongClickLabel = "Long click",
+ label = { Text("Child Button") },
+ secondaryLabel = { Text("with long click") },
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
}
}
@@ -288,7 +508,7 @@
colors = ButtonDefaults.buttonColors(),
modifier = Modifier.fillMaxWidth()
) {
- Text("Compact Button", maxLines = 1, overflow = TextOverflow.Ellipsis)
+ Text("Compact Button", modifier = Modifier.fillMaxWidth())
}
}
item {
@@ -297,7 +517,7 @@
colors = ButtonDefaults.filledVariantButtonColors(),
modifier = Modifier.fillMaxWidth()
) {
- Text("Filled Variant", maxLines = 1, overflow = TextOverflow.Ellipsis)
+ Text("Filled Variant", modifier = Modifier.fillMaxWidth())
}
}
item {
@@ -306,7 +526,7 @@
colors = ButtonDefaults.filledTonalButtonColors(),
modifier = Modifier.fillMaxWidth()
) {
- Text("Filled Tonal", maxLines = 1, overflow = TextOverflow.Ellipsis)
+ Text("Filled Tonal", modifier = Modifier.fillMaxWidth())
}
}
item {
@@ -316,7 +536,7 @@
border = ButtonDefaults.outlinedButtonBorder(enabled = true),
modifier = Modifier.fillMaxWidth()
) {
- Text("Outlined", maxLines = 1, overflow = TextOverflow.Ellipsis)
+ Text("Outlined", modifier = Modifier.fillMaxWidth())
}
}
item { ListHeader { Text("Icon and Label") } }
@@ -324,7 +544,7 @@
item {
CompactButton(
onClick = { /* Do something */ },
- icon = { StandardIcon(ButtonDefaults.SmallIconSize) },
+ icon = { FavoriteIcon(ButtonDefaults.SmallIconSize) },
colors = ButtonDefaults.filledVariantButtonColors(),
modifier = Modifier.fillMaxWidth()
) {
@@ -334,7 +554,7 @@
item {
CompactButton(
onClick = { /* Do something */ },
- icon = { StandardIcon(ButtonDefaults.SmallIconSize) },
+ icon = { FavoriteIcon(ButtonDefaults.SmallIconSize) },
colors = ButtonDefaults.filledTonalButtonColors(),
modifier = Modifier.fillMaxWidth()
) {
@@ -344,7 +564,7 @@
item {
CompactButton(
onClick = { /* Do something */ },
- icon = { StandardIcon(ButtonDefaults.SmallIconSize) },
+ icon = { FavoriteIcon(ButtonDefaults.SmallIconSize) },
colors = ButtonDefaults.outlinedButtonColors(),
border = ButtonDefaults.outlinedButtonBorder(enabled = true),
modifier = Modifier.fillMaxWidth()
@@ -355,7 +575,7 @@
item {
CompactButton(
onClick = { /* Do something */ },
- icon = { StandardIcon(ButtonDefaults.SmallIconSize) },
+ icon = { FavoriteIcon(ButtonDefaults.SmallIconSize) },
colors = ButtonDefaults.childButtonColors(),
modifier = Modifier.fillMaxWidth()
) {
@@ -366,20 +586,20 @@
item {
CompactButton(
onClick = { /* Do something */ },
- icon = { StandardIcon(ButtonDefaults.SmallIconSize) },
+ icon = { FavoriteIcon(ButtonDefaults.SmallIconSize) },
)
}
item {
CompactButton(
onClick = { /* Do something */ },
- icon = { StandardIcon(ButtonDefaults.SmallIconSize) },
+ icon = { FavoriteIcon(ButtonDefaults.SmallIconSize) },
colors = ButtonDefaults.filledTonalButtonColors(),
)
}
item {
CompactButton(
onClick = { /* Do something */ },
- icon = { StandardIcon(ButtonDefaults.SmallIconSize) },
+ icon = { FavoriteIcon(ButtonDefaults.SmallIconSize) },
colors = ButtonDefaults.outlinedButtonColors(),
border = ButtonDefaults.outlinedButtonBorder(enabled = true),
)
@@ -387,7 +607,7 @@
item {
CompactButton(
onClick = { /* Do something */ },
- icon = { StandardIcon(ButtonDefaults.SmallIconSize) },
+ icon = { FavoriteIcon(ButtonDefaults.SmallIconSize) },
colors = ButtonDefaults.childButtonColors(),
)
}
@@ -409,59 +629,76 @@
item { ListHeader { Text("3 line label") } }
item { MultilineButton(enabled = true) }
item { MultilineButton(enabled = false) }
- item { MultilineButton(enabled = true, icon = { StandardIcon(ButtonDefaults.IconSize) }) }
- item { MultilineButton(enabled = false, icon = { StandardIcon(ButtonDefaults.IconSize) }) }
+ item { MultilineButton(enabled = true, icon = { FavoriteIcon(ButtonDefaults.IconSize) }) }
+ item { MultilineButton(enabled = false, icon = { FavoriteIcon(ButtonDefaults.IconSize) }) }
item { ListHeader { Text("5 line button") } }
item { Multiline3SlotButton(enabled = true) }
item { Multiline3SlotButton(enabled = false) }
item {
- Multiline3SlotButton(enabled = true, icon = { StandardIcon(ButtonDefaults.IconSize) })
+ Multiline3SlotButton(enabled = true, icon = { FavoriteIcon(ButtonDefaults.IconSize) })
}
item {
- Multiline3SlotButton(enabled = false, icon = { StandardIcon(ButtonDefaults.IconSize) })
+ Multiline3SlotButton(enabled = false, icon = { FavoriteIcon(ButtonDefaults.IconSize) })
}
}
}
@Composable
-fun AvatarButtonDemo() {
- ScalingLazyDemo {
- item { ListHeader { Text("Label + Avatar") } }
- item { AvatarButton(enabled = true) }
- item { AvatarButton(enabled = false) }
- item { ListHeader { Text("Primary/Secondary + Avatar") } }
- item { Avatar3SlotButton(enabled = true) }
- item { Avatar3SlotButton(enabled = false) }
- }
-}
-
-@Composable
fun ButtonBackgroundImageDemo() {
ScalingLazyDemo {
item { ListHeader { Text("Button (Image Background)") } }
item { ButtonBackgroundImage(painterResource(R.drawable.card_background), enabled = true) }
item { ButtonBackgroundImage(painterResource(R.drawable.card_background), enabled = false) }
+ item { ListHeader { Text("2 Slot Button") } }
+ item {
+ Button(
+ modifier = Modifier.sizeIn(maxHeight = ButtonDefaults.Height).fillMaxWidth(),
+ onClick = { /* Do something */ },
+ label = { Text("Label", maxLines = 1) },
+ secondaryLabel = { Text("Secondary label", maxLines = 1) },
+ colors =
+ ButtonDefaults.imageBackgroundButtonColors(
+ painterResource(R.drawable.card_background)
+ )
+ )
+ }
+ item {
+ Button(
+ modifier = Modifier.sizeIn(maxHeight = ButtonDefaults.Height).fillMaxWidth(),
+ onClick = { /* Do something */ },
+ label = { Text("Label", maxLines = 1) },
+ secondaryLabel = { Text("Secondary label", maxLines = 1) },
+ enabled = false,
+ colors =
+ ButtonDefaults.imageBackgroundButtonColors(
+ painterResource(R.drawable.card_background)
+ )
+ )
+ }
}
}
@Composable
-private fun AvatarButton(enabled: Boolean) =
- MultilineButton(
- enabled = enabled,
- colors = ButtonDefaults.filledTonalButtonColors(),
- icon = { AvatarIcon() },
- label = { Text("Primary text") }
- )
+fun AppButtonDemo() {
+ ScalingLazyDemo {
+ item { ListHeader { Text("Large Icon") } }
+ item { ButtonLargeIcon(enabled = true) }
+ item { ButtonLargeIcon(enabled = false) }
+ item { ButtonLargeIconSample(enabled = true) }
+ item { ButtonLargeIconSample(enabled = false) }
+ }
+}
@Composable
-private fun Avatar3SlotButton(enabled: Boolean) =
- Multiline3SlotButton(
- enabled = enabled,
- colors = ButtonDefaults.filledTonalButtonColors(),
- icon = { AvatarIcon() },
- label = { Text("Primary text") },
- secondaryLabel = { Text("Secondary label") }
- )
+fun AvatarButtonDemo() {
+ ScalingLazyDemo {
+ item { ListHeader { Text("Extra Large Icon") } }
+ item { ButtonExtraLargeIcon(enabled = true) }
+ item { ButtonExtraLargeIcon(enabled = false) }
+ item { ButtonExtraLargeIconSample(enabled = true) }
+ item { ButtonExtraLargeIconSample(enabled = false) }
+ }
+}
@Composable
private fun MultilineButton(
@@ -522,9 +759,33 @@
@Composable
private fun ButtonBackgroundImage(painter: Painter, enabled: Boolean) =
Button(
- modifier = Modifier.sizeIn(maxHeight = ButtonDefaults.Height),
+ modifier = Modifier.sizeIn(maxHeight = ButtonDefaults.Height).fillMaxWidth(),
onClick = { /* Do something */ },
label = { Text("Label", maxLines = 1) },
enabled = enabled,
colors = ButtonDefaults.imageBackgroundButtonColors(painter)
)
+
+@Composable
+private fun ButtonLargeIcon(enabled: Boolean = true) {
+ Button(
+ onClick = { /* Do something */ },
+ label = { Text("Button") },
+ icon = { FavoriteIcon(ButtonDefaults.LargeIconSize) },
+ enabled = enabled,
+ modifier = Modifier.fillMaxWidth(),
+ contentPadding = ButtonDefaults.ButtonWithLargeIconContentPadding
+ )
+}
+
+@Composable
+private fun ButtonExtraLargeIcon(enabled: Boolean = true) {
+ Button(
+ onClick = { /* Do something */ },
+ label = { Text("Button") },
+ icon = { FavoriteIcon(ButtonDefaults.ExtraLargeIconSize) },
+ enabled = enabled,
+ modifier = Modifier.fillMaxWidth(),
+ contentPadding = ButtonDefaults.ButtonWithExtraLargeIconContentPadding
+ )
+}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/CheckboxButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/CheckboxButtonDemo.kt
index 784d022..1c98c7f 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/CheckboxButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/CheckboxButtonDemo.kt
@@ -29,7 +29,7 @@
import androidx.wear.compose.material3.CheckboxButton
import androidx.wear.compose.material3.Icon
import androidx.wear.compose.material3.ListHeader
-import androidx.wear.compose.material3.LocalTextMaxLines
+import androidx.wear.compose.material3.LocalTextConfiguration
import androidx.wear.compose.material3.Text
@Composable
@@ -113,7 +113,7 @@
Text(
primary,
modifier = Modifier.fillMaxWidth(),
- maxLines = primaryMaxLines ?: LocalTextMaxLines.current
+ maxLines = primaryMaxLines ?: LocalTextConfiguration.current.maxLines
)
},
secondaryLabel = {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/EdgeButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/EdgeButtonDemo.kt
index 14317c3..099f0b5 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/EdgeButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/EdgeButtonDemo.kt
@@ -57,6 +57,7 @@
import androidx.wear.compose.material3.Text
import androidx.wear.compose.material3.TextButton
import androidx.wear.compose.material3.TextButtonDefaults
+import androidx.wear.compose.material3.samples.icons.CheckIcon
@Composable
fun EdgeButtonBelowLazyColumnDemo() {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconButtonDemo.kt
index 0d75c07..a28a8f0 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconButtonDemo.kt
@@ -46,6 +46,7 @@
import androidx.wear.compose.material3.samples.IconButtonWithImageSample
import androidx.wear.compose.material3.samples.IconButtonWithOnLongClickSample
import androidx.wear.compose.material3.samples.OutlinedIconButtonSample
+import androidx.wear.compose.material3.samples.icons.FavoriteIcon
import androidx.wear.compose.material3.touchTargetAwareSize
@Composable
@@ -57,7 +58,7 @@
Row {
IconButtonSample()
Spacer(modifier = Modifier.width(5.dp))
- IconButton(onClick = {}, enabled = false) { StandardIcon(ButtonDefaults.IconSize) }
+ IconButton(onClick = {}, enabled = false) { FavoriteIcon(ButtonDefaults.IconSize) }
}
}
item { ListHeader { Text("Filled Tonal") } }
@@ -66,7 +67,7 @@
FilledTonalIconButtonSample()
Spacer(modifier = Modifier.width(5.dp))
FilledTonalIconButton(onClick = {}, enabled = false) {
- StandardIcon(ButtonDefaults.IconSize)
+ FavoriteIcon(ButtonDefaults.IconSize)
}
}
}
@@ -76,7 +77,7 @@
FilledIconButtonSample()
Spacer(modifier = Modifier.width(5.dp))
FilledIconButton(onClick = {}, enabled = false) {
- StandardIcon(ButtonDefaults.IconSize)
+ FavoriteIcon(ButtonDefaults.IconSize)
}
}
}
@@ -90,7 +91,7 @@
enabled = false,
colors = IconButtonDefaults.filledVariantIconButtonColors()
) {
- StandardIcon(ButtonDefaults.IconSize)
+ FavoriteIcon(ButtonDefaults.IconSize)
}
}
}
@@ -100,7 +101,7 @@
OutlinedIconButtonSample()
Spacer(modifier = Modifier.width(5.dp))
OutlinedIconButton(onClick = {}, enabled = false) {
- StandardIcon(ButtonDefaults.IconSize)
+ FavoriteIcon(ButtonDefaults.IconSize)
}
}
}
@@ -128,7 +129,7 @@
),
interactionSource = interactionSource1
) {
- StandardIcon(ButtonDefaults.IconSize)
+ FavoriteIcon(ButtonDefaults.IconSize)
}
Spacer(modifier = Modifier.width(5.dp))
val interactionSource2 = remember { MutableInteractionSource() }
@@ -143,7 +144,7 @@
),
interactionSource = interactionSource2
) {
- StandardIcon(ButtonDefaults.IconSize)
+ FavoriteIcon(ButtonDefaults.IconSize)
}
}
}
@@ -222,6 +223,6 @@
modifier = Modifier.touchTargetAwareSize(size),
onClick = { /* Do something */ }
) {
- StandardIcon(IconButtonDefaults.iconSizeFor(size))
+ FavoriteIcon(IconButtonDefaults.iconSizeFor(size))
}
}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ProgressIndicatorDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ProgressIndicatorDemo.kt
index 73f8f8e..b8370ff 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ProgressIndicatorDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ProgressIndicatorDemo.kt
@@ -21,9 +21,15 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -34,10 +40,16 @@
import androidx.wear.compose.material3.Button
import androidx.wear.compose.material3.CircularProgressIndicator
import androidx.wear.compose.material3.CircularProgressIndicatorDefaults
+import androidx.wear.compose.material3.ExperimentalWearMaterial3Api
+import androidx.wear.compose.material3.Icon
import androidx.wear.compose.material3.IconButtonDefaults
+import androidx.wear.compose.material3.InlineSlider
+import androidx.wear.compose.material3.InlineSliderDefaults
import androidx.wear.compose.material3.ListHeader
import androidx.wear.compose.material3.MaterialTheme
import androidx.wear.compose.material3.ProgressIndicatorDefaults
+import androidx.wear.compose.material3.SegmentedCircularProgressIndicator
+import androidx.wear.compose.material3.SwitchButton
import androidx.wear.compose.material3.Text
import androidx.wear.compose.material3.samples.FullScreenProgressIndicatorSample
import androidx.wear.compose.material3.samples.LinearProgressIndicatorSample
@@ -45,6 +57,7 @@
import androidx.wear.compose.material3.samples.OverflowProgressIndicatorSample
import androidx.wear.compose.material3.samples.SegmentedProgressIndicatorOnOffSample
import androidx.wear.compose.material3.samples.SegmentedProgressIndicatorSample
+import androidx.wear.compose.material3.samples.SmallSegmentedProgressIndicatorSample
import androidx.wear.compose.material3.samples.SmallValuesProgressIndicatorSample
val ProgressIndicatorDemos =
@@ -81,6 +94,15 @@
ComposableDemo("Progress segments on/off") {
Centralize { SegmentedProgressIndicatorOnOffSample() }
},
+ ComposableDemo("Small segmented progress") {
+ Centralize { SmallSegmentedProgressIndicatorSample() }
+ },
+ ComposableDemo("Custom circular progress") {
+ Centralize { CircularProgressCustomisableFullScreenDemo() }
+ },
+ ComposableDemo("Custom segmented progress") {
+ Centralize { SegmentedProgressCustomisableFullScreenDemo() }
+ },
ComposableDemo("Linear progress indicator") {
Centralize { LinearProgressIndicatorSamples() }
},
@@ -111,3 +133,208 @@
}
}
}
+
+@Composable
+fun CircularProgressCustomisableFullScreenDemo() {
+ val progress = remember { mutableFloatStateOf(0.4f) }
+ val startAngle = remember { mutableFloatStateOf(360f) }
+ val endAngle = remember { mutableFloatStateOf(360f) }
+ val enabled = remember { mutableStateOf(true) }
+ val hasLargeStroke = remember { mutableStateOf(true) }
+ val hasCustomColors = remember { mutableStateOf(false) }
+ val colors =
+ if (hasCustomColors.value) {
+ ProgressIndicatorDefaults.colors(
+ indicatorColor = Color.Green,
+ trackColor = Color.Green.copy(alpha = 0.5f)
+ )
+ } else {
+ ProgressIndicatorDefaults.colors()
+ }
+ val strokeWidth =
+ if (hasLargeStroke.value) CircularProgressIndicatorDefaults.largeStrokeWidth
+ else CircularProgressIndicatorDefaults.smallStrokeWidth
+
+ Box(
+ modifier =
+ Modifier.background(MaterialTheme.colorScheme.background)
+ .padding(CircularProgressIndicatorDefaults.FullScreenPadding)
+ .fillMaxSize()
+ ) {
+ ProgressIndicatorCustomizer(
+ progress = progress,
+ startAngle = startAngle,
+ endAngle = endAngle,
+ enabled = enabled,
+ hasLargeStroke = hasLargeStroke,
+ hasCustomColors = hasCustomColors,
+ )
+
+ CircularProgressIndicator(
+ progress = { progress.value },
+ startAngle = startAngle.value,
+ endAngle = endAngle.value,
+ enabled = enabled.value,
+ strokeWidth = strokeWidth,
+ colors = colors
+ )
+ }
+}
+
+@Composable
+fun SegmentedProgressCustomisableFullScreenDemo() {
+ val progress = remember { mutableFloatStateOf(0f) }
+ val startAngle = remember { mutableFloatStateOf(0f) }
+ val endAngle = remember { mutableFloatStateOf(0f) }
+ val enabled = remember { mutableStateOf(true) }
+ val hasCustomColors = remember { mutableStateOf(false) }
+ val hasLargeStroke = remember { mutableStateOf(true) }
+ val numSegments = remember { mutableIntStateOf(5) }
+ val colors =
+ if (hasCustomColors.value) {
+ ProgressIndicatorDefaults.colors(
+ indicatorColor = Color.Green,
+ trackColor = Color.Green.copy(alpha = 0.5f)
+ )
+ } else {
+ ProgressIndicatorDefaults.colors()
+ }
+ val strokeWidth =
+ if (hasLargeStroke.value) CircularProgressIndicatorDefaults.largeStrokeWidth
+ else CircularProgressIndicatorDefaults.smallStrokeWidth
+
+ Box(
+ modifier =
+ Modifier.background(MaterialTheme.colorScheme.background)
+ .padding(CircularProgressIndicatorDefaults.FullScreenPadding)
+ .fillMaxSize()
+ ) {
+ ProgressIndicatorCustomizer(
+ progress = progress,
+ startAngle = startAngle,
+ endAngle = endAngle,
+ enabled = enabled,
+ hasLargeStroke = hasLargeStroke,
+ hasCustomColors = hasCustomColors,
+ numSegments = numSegments,
+ )
+
+ SegmentedCircularProgressIndicator(
+ segmentCount = numSegments.value,
+ progress = { progress.value },
+ startAngle = startAngle.value,
+ endAngle = endAngle.value,
+ enabled = enabled.value,
+ strokeWidth = strokeWidth,
+ colors = colors
+ )
+ }
+}
+
+@OptIn(ExperimentalWearMaterial3Api::class)
+@Composable
+fun ProgressIndicatorCustomizer(
+ progress: MutableState<Float>,
+ startAngle: MutableState<Float>,
+ endAngle: MutableState<Float>,
+ enabled: MutableState<Boolean>,
+ hasLargeStroke: MutableState<Boolean>,
+ hasCustomColors: MutableState<Boolean>,
+ numSegments: MutableState<Int>? = null,
+) {
+ ScalingLazyColumn(
+ modifier = Modifier.fillMaxSize().padding(12.dp),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ item { Text(String.format("Progress: %.0f%%", progress.value * 100)) }
+ item {
+ InlineSlider(
+ value = progress.value,
+ onValueChange = { progress.value = it },
+ increaseIcon = { Icon(InlineSliderDefaults.Increase, "Increase") },
+ decreaseIcon = { Icon(InlineSliderDefaults.Decrease, "Decrease") },
+ valueRange = 0f..1f,
+ steps = 4,
+ colors =
+ InlineSliderDefaults.colors(
+ containerColor = MaterialTheme.colorScheme.background,
+ ),
+ segmented = false
+ )
+ }
+ if (numSegments != null) {
+ item { Text("Segments: ${numSegments.value}") }
+ item {
+ InlineSlider(
+ value = numSegments.value.toFloat(),
+ onValueChange = { numSegments.value = it.toInt() },
+ increaseIcon = { Icon(InlineSliderDefaults.Increase, "Increase") },
+ decreaseIcon = { Icon(InlineSliderDefaults.Decrease, "Decrease") },
+ valueRange = 1f..12f,
+ steps = 10,
+ colors =
+ InlineSliderDefaults.colors(
+ containerColor = MaterialTheme.colorScheme.background,
+ ),
+ )
+ }
+ }
+ item { Text("Start Angle: ${startAngle.value.toInt()}") }
+ item {
+ InlineSlider(
+ value = startAngle.value,
+ onValueChange = { startAngle.value = it },
+ increaseIcon = { Icon(InlineSliderDefaults.Increase, "Increase") },
+ decreaseIcon = { Icon(InlineSliderDefaults.Decrease, "Decrease") },
+ valueRange = 0f..360f,
+ steps = 7,
+ segmented = false,
+ colors =
+ InlineSliderDefaults.colors(
+ containerColor = MaterialTheme.colorScheme.background,
+ ),
+ )
+ }
+ item { Text("End angle: ${endAngle.value.toInt()}") }
+ item {
+ InlineSlider(
+ value = endAngle.value,
+ onValueChange = { endAngle.value = it },
+ increaseIcon = { Icon(InlineSliderDefaults.Increase, "Increase") },
+ decreaseIcon = { Icon(InlineSliderDefaults.Decrease, "Decrease") },
+ valueRange = 0f..360f,
+ steps = 7,
+ segmented = false,
+ colors =
+ InlineSliderDefaults.colors(
+ containerColor = MaterialTheme.colorScheme.background,
+ ),
+ )
+ }
+ item {
+ SwitchButton(
+ modifier = Modifier.fillMaxWidth().padding(8.dp),
+ checked = enabled.value,
+ onCheckedChange = { enabled.value = it },
+ label = { Text("Enabled") },
+ )
+ }
+ item {
+ SwitchButton(
+ modifier = Modifier.fillMaxWidth().padding(8.dp),
+ checked = hasLargeStroke.value,
+ onCheckedChange = { hasLargeStroke.value = it },
+ label = { Text("Large stroke") },
+ )
+ }
+ item {
+ SwitchButton(
+ modifier = Modifier.fillMaxWidth().padding(8.dp),
+ checked = hasCustomColors.value,
+ onCheckedChange = { hasCustomColors.value = it },
+ label = { Text("Custom colors") },
+ )
+ }
+ }
+}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/RadioButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/RadioButtonDemo.kt
index 34aa8bb..7ff3e1c 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/RadioButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/RadioButtonDemo.kt
@@ -32,7 +32,7 @@
import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.material3.Icon
import androidx.wear.compose.material3.ListHeader
-import androidx.wear.compose.material3.LocalTextMaxLines
+import androidx.wear.compose.material3.LocalTextConfiguration
import androidx.wear.compose.material3.RadioButton
import androidx.wear.compose.material3.Text
@@ -139,7 +139,7 @@
Text(
primary,
Modifier.fillMaxWidth(),
- maxLines = primaryMaxLines ?: LocalTextMaxLines.current
+ maxLines = primaryMaxLines ?: LocalTextConfiguration.current.maxLines
)
},
secondaryLabel =
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitCheckboxButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitCheckboxButtonDemo.kt
index 55249c0..82e0e68 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitCheckboxButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitCheckboxButtonDemo.kt
@@ -26,7 +26,7 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.wear.compose.material3.ListHeader
-import androidx.wear.compose.material3.LocalTextMaxLines
+import androidx.wear.compose.material3.LocalTextConfiguration
import androidx.wear.compose.material3.SplitCheckboxButton
import androidx.wear.compose.material3.Text
@@ -99,7 +99,7 @@
Text(
primary,
modifier = Modifier.fillMaxWidth(),
- maxLines = primaryMaxLines ?: LocalTextMaxLines.current
+ maxLines = primaryMaxLines ?: LocalTextConfiguration.current.maxLines
)
},
secondaryLabel =
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitRadioButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitRadioButtonDemo.kt
index db4de04..b5493b5 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitRadioButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitRadioButtonDemo.kt
@@ -26,7 +26,7 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.wear.compose.material3.ListHeader
-import androidx.wear.compose.material3.LocalTextMaxLines
+import androidx.wear.compose.material3.LocalTextConfiguration
import androidx.wear.compose.material3.SplitRadioButton
import androidx.wear.compose.material3.Text
@@ -105,7 +105,7 @@
Text(
primary,
Modifier.fillMaxWidth(),
- maxLines = primaryMaxLines ?: LocalTextMaxLines.current
+ maxLines = primaryMaxLines ?: LocalTextConfiguration.current.maxLines
)
},
secondaryLabel =
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitSwitchButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitSwitchButtonDemo.kt
index 5388dd2..eaa636d 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitSwitchButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitSwitchButtonDemo.kt
@@ -26,7 +26,7 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.wear.compose.material3.ListHeader
-import androidx.wear.compose.material3.LocalTextMaxLines
+import androidx.wear.compose.material3.LocalTextConfiguration
import androidx.wear.compose.material3.SplitSwitchButton
import androidx.wear.compose.material3.Text
@@ -90,7 +90,7 @@
Text(
primary,
modifier = Modifier.fillMaxWidth(),
- maxLines = primaryMaxLines ?: LocalTextMaxLines.current
+ maxLines = primaryMaxLines ?: LocalTextConfiguration.current.maxLines
)
},
secondaryLabel =
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SwitchButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SwitchButtonDemo.kt
index b92ffdf..6ba5e6f 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SwitchButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SwitchButtonDemo.kt
@@ -28,7 +28,7 @@
import androidx.compose.ui.Modifier
import androidx.wear.compose.material3.Icon
import androidx.wear.compose.material3.ListHeader
-import androidx.wear.compose.material3.LocalTextMaxLines
+import androidx.wear.compose.material3.LocalTextConfiguration
import androidx.wear.compose.material3.SwitchButton
import androidx.wear.compose.material3.Text
@@ -113,7 +113,7 @@
Text(
primary,
modifier = Modifier.fillMaxWidth(),
- maxLines = primaryMaxLines ?: LocalTextMaxLines.current,
+ maxLines = primaryMaxLines ?: LocalTextConfiguration.current.maxLines,
)
},
secondaryLabel = {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TypographyDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TypographyDemo.kt
index 631150f..8bca579 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TypographyDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TypographyDemo.kt
@@ -16,31 +16,57 @@
package androidx.wear.compose.material3.demos
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+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.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.wear.compose.foundation.CurvedDirection
import androidx.wear.compose.foundation.CurvedLayout
import androidx.wear.compose.foundation.CurvedTextStyle
import androidx.wear.compose.integration.demos.common.Centralize
import androidx.wear.compose.integration.demos.common.ComposableDemo
import androidx.wear.compose.integration.demos.common.DemoCategory
+import androidx.wear.compose.material3.CurvedTextDefaults
+import androidx.wear.compose.material3.ExperimentalWearMaterial3Api
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.InlineSlider
+import androidx.wear.compose.material3.InlineSliderDefaults
import androidx.wear.compose.material3.MaterialTheme
import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.TextToggleButton
import androidx.wear.compose.material3.curvedText
+@OptIn(ExperimentalWearMaterial3Api::class)
var TypographyDemos =
listOf(
DemoCategory(
"Arc",
listOf(
ComposableDemo("Arc Small") {
- val curvedStyle = CurvedTextStyle(MaterialTheme.typography.arcSmall)
- CurvedLayout { curvedText("Arc Small", style = curvedStyle) }
+ ArcWithLetterSpacing(MaterialTheme.typography.arcSmall, "Arc Small")
},
ComposableDemo("Arc Medium") {
- val curvedStyle = CurvedTextStyle(MaterialTheme.typography.arcMedium)
- CurvedLayout { curvedText("Arc Medium", style = curvedStyle) }
- }
+ ArcWithLetterSpacing(MaterialTheme.typography.arcMedium, "Arc Medium")
+ },
+ ComposableDemo("Arc Large") {
+ ArcWithLetterSpacing(MaterialTheme.typography.arcLarge, "Arc Large")
+ },
)
),
DemoCategory(
@@ -124,3 +150,71 @@
)
),
)
+
+@OptIn(ExperimentalWearMaterial3Api::class)
+@Composable
+private fun ArcWithLetterSpacing(arcStyle: TextStyle, label: String) {
+ var topLetterSpacing by remember { mutableStateOf(0.6f) }
+ var bottomLetterSpacing by remember { mutableStateOf(2.0f) }
+ val topCurvedStyle = CurvedTextStyle(arcStyle).copy(letterSpacing = topLetterSpacing.sp)
+ val bottomCurvedStyle = CurvedTextStyle(arcStyle).copy(letterSpacing = bottomLetterSpacing.sp)
+ val mmms = "MMMMMMMMMMMMMMMMMMMM"
+ var useMMMs by remember { mutableStateOf(true) }
+
+ Box {
+ CurvedLayout {
+ curvedText(
+ if (useMMMs) mmms else label,
+ style = topCurvedStyle,
+ maxSweepAngle = CurvedTextDefaults.StaticContentMaxSweepAngle,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ CurvedLayout(anchor = 90f, angularDirection = CurvedDirection.Angular.Reversed) {
+ curvedText(
+ if (useMMMs) mmms else label,
+ style = bottomCurvedStyle,
+ maxSweepAngle = CurvedTextDefaults.StaticContentMaxSweepAngle,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+
+ Column(
+ modifier = Modifier.fillMaxSize().padding(32.dp),
+ verticalArrangement = Arrangement.Center
+ ) {
+ Text(
+ "Top=$topLetterSpacing, bottom = $bottomLetterSpacing",
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth()
+ )
+ InlineSlider(
+ value = topLetterSpacing,
+ onValueChange = { topLetterSpacing = it },
+ increaseIcon = { Icon(InlineSliderDefaults.Increase, "Increase") },
+ decreaseIcon = { Icon(InlineSliderDefaults.Decrease, "Decrease") },
+ valueRange = 0f..4f,
+ steps = 39,
+ segmented = false
+ )
+ InlineSlider(
+ value = bottomLetterSpacing,
+ onValueChange = { bottomLetterSpacing = it },
+ increaseIcon = { Icon(InlineSliderDefaults.Increase, "Increase") },
+ decreaseIcon = { Icon(InlineSliderDefaults.Decrease, "Decrease") },
+ valueRange = 0f..4f,
+ steps = 39,
+ segmented = false
+ )
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
+ TextToggleButton(
+ checked = useMMMs,
+ onCheckedChange = { useMMMs = !useMMMs },
+ modifier = Modifier.height(36.dp)
+ ) {
+ Text(text = "MMM")
+ }
+ }
+ }
+ }
+}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
index 2f9ce14..eae5eac 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
@@ -43,6 +43,22 @@
Material3DemoCategory(
"Material 3",
listOf(
+ Material3DemoCategory(title = "Typography", TypographyDemos),
+ Material3DemoCategory(
+ "Button",
+ listOf(
+ ComposableDemo("Base Button") { BaseButtonDemo() },
+ ComposableDemo("Filled Button") { ButtonDemo() },
+ ComposableDemo("Filled Tonal Button") { FilledTonalButtonDemo() },
+ ComposableDemo("Filled Variant Button") { FilledVariantButtonDemo() },
+ ComposableDemo("Outlined Button") { OutlinedButtonDemo() },
+ ComposableDemo("Child Button") { ChildButtonDemo() },
+ ComposableDemo("Multiline Button") { MultilineButtonDemo() },
+ ComposableDemo("App Button") { AppButtonDemo() },
+ ComposableDemo("Avatar Button") { AvatarButtonDemo() },
+ ComposableDemo("Button (Image Background)") { ButtonBackgroundImageDemo() },
+ )
+ ),
ComposableDemo("Color Scheme") { ColorSchemeDemos() },
Material3DemoCategory("Curved Text", CurvedTextDemos),
Material3DemoCategory("Alert Dialog", AlertDialogs),
@@ -51,19 +67,6 @@
ComposableDemo("Scaffold") { ScaffoldSample() },
Material3DemoCategory("ScrollAway", ScrollAwayDemos),
ComposableDemo("Haptics") { Centralize { HapticsDemos() } },
- Material3DemoCategory(
- "Button",
- listOf(
- ComposableDemo("Filled Button") { ButtonDemo() },
- ComposableDemo("Filled Tonal Button") { FilledTonalButtonDemo() },
- ComposableDemo("Filled Variant Button") { FilledVariantButtonDemo() },
- ComposableDemo("Outlined Button") { OutlinedButtonDemo() },
- ComposableDemo("Child Button") { ChildButtonDemo() },
- ComposableDemo("Multiline Button") { MultilineButtonDemo() },
- ComposableDemo("Avatar Button") { AvatarButtonDemo() },
- ComposableDemo("Button (Image Background)") { ButtonBackgroundImageDemo() },
- )
- ),
ComposableDemo("Compact Button") { CompactButtonDemo() },
ComposableDemo("Icon Button") { IconButtonDemo() },
ComposableDemo("Image Button") { ImageButtonDemo() },
@@ -150,7 +153,6 @@
},
)
),
- Material3DemoCategory(title = "Typography", TypographyDemos),
Material3DemoCategory(
"Animated Text",
if (Build.VERSION.SDK_INT > 31) {
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ButtonSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ButtonSample.kt
index b751fc8..64906ea 100644
--- a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ButtonSample.kt
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ButtonSample.kt
@@ -21,9 +21,9 @@
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
-import androidx.compose.material.icons.filled.Favorite
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.wear.compose.material3.Button
import androidx.wear.compose.material3.ButtonDefaults
@@ -42,23 +42,6 @@
@Sampled
@Composable
-fun ButtonWithOnLongClickSample(
- onClickHandler: () -> Unit,
- onLongClickHandler: () -> Unit,
- modifier: Modifier = Modifier.fillMaxWidth(),
-) {
- Button(
- onClick = onClickHandler,
- onLongClick = onLongClickHandler,
- onLongClickLabel = "Long click",
- label = { Text("Button") },
- secondaryLabel = { Text("with long click") },
- modifier = modifier,
- )
-}
-
-@Sampled
-@Composable
fun ButtonSample(modifier: Modifier = Modifier.fillMaxWidth()) {
Button(
onClick = { /* Do something */ },
@@ -66,7 +49,7 @@
secondaryLabel = { Text("Secondary label") },
icon = {
Icon(
- Icons.Filled.Favorite,
+ painter = painterResource(R.drawable.ic_favorite_rounded),
contentDescription = "Favorite icon",
modifier = Modifier.size(ButtonDefaults.IconSize)
)
@@ -77,6 +60,53 @@
@Sampled
@Composable
+fun ButtonLargeIconSample(modifier: Modifier = Modifier.fillMaxWidth(), enabled: Boolean = true) {
+ // When customising the icon size, it is recommended to also specify
+ // the associated content padding
+ Button(
+ onClick = { /* Do something */ },
+ enabled = enabled,
+ label = { Text("Button") },
+ secondaryLabel = { Text("Secondary label") },
+ icon = {
+ Icon(
+ painter = painterResource(R.drawable.ic_favorite_rounded),
+ contentDescription = "Favorite icon",
+ modifier = Modifier.size(ButtonDefaults.LargeIconSize)
+ )
+ },
+ contentPadding = ButtonDefaults.ButtonWithLargeIconContentPadding,
+ modifier = modifier
+ )
+}
+
+@Sampled
+@Composable
+fun ButtonExtraLargeIconSample(
+ modifier: Modifier = Modifier.fillMaxWidth(),
+ enabled: Boolean = true
+) {
+ // When customising the icon size, it is recommended to also specify
+ // the associated content padding
+ Button(
+ onClick = { /* Do something */ },
+ enabled = enabled,
+ label = { Text("Button") },
+ secondaryLabel = { Text("Secondary label") },
+ icon = {
+ Icon(
+ painter = painterResource(R.drawable.ic_favorite_rounded),
+ contentDescription = "Favorite icon",
+ modifier = Modifier.size(ButtonDefaults.ExtraLargeIconSize)
+ )
+ },
+ contentPadding = ButtonDefaults.ButtonWithExtraLargeIconContentPadding,
+ modifier = modifier
+ )
+}
+
+@Sampled
+@Composable
fun SimpleFilledTonalButtonSample(modifier: Modifier = Modifier.fillMaxWidth()) {
FilledTonalButton(
onClick = { /* Do something */ },
@@ -87,23 +117,6 @@
@Sampled
@Composable
-fun FilledTonalButtonWithOnLongClickSample(
- onClickHandler: () -> Unit,
- onLongClickHandler: () -> Unit,
- modifier: Modifier = Modifier.fillMaxWidth()
-) {
- FilledTonalButton(
- onClick = onClickHandler,
- onLongClick = onLongClickHandler,
- onLongClickLabel = "Long click",
- label = { Text("Filled Tonal Button") },
- secondaryLabel = { Text("with long click") },
- modifier = modifier,
- )
-}
-
-@Sampled
-@Composable
fun FilledTonalButtonSample(modifier: Modifier = Modifier.fillMaxWidth()) {
FilledTonalButton(
onClick = { /* Do something */ },
@@ -111,7 +124,7 @@
secondaryLabel = { Text("Secondary label") },
icon = {
Icon(
- Icons.Filled.Favorite,
+ painter = painterResource(R.drawable.ic_favorite_rounded),
contentDescription = "Favorite icon",
modifier = Modifier.size(ButtonDefaults.IconSize)
)
@@ -141,7 +154,7 @@
secondaryLabel = { Text("Secondary label") },
icon = {
Icon(
- Icons.Filled.Favorite,
+ painter = painterResource(R.drawable.ic_favorite_rounded),
contentDescription = "Favorite icon",
modifier = Modifier.size(ButtonDefaults.IconSize)
)
@@ -162,23 +175,6 @@
@Sampled
@Composable
-fun OutlinedButtonWithOnLongClickSample(
- onClickHandler: () -> Unit,
- onLongClickHandler: () -> Unit,
- modifier: Modifier = Modifier.fillMaxWidth()
-) {
- OutlinedButton(
- onClick = onClickHandler,
- onLongClick = onLongClickHandler,
- onLongClickLabel = "Long click",
- label = { Text("Outlined Button") },
- secondaryLabel = { Text("with long click") },
- modifier = modifier,
- )
-}
-
-@Sampled
-@Composable
fun OutlinedButtonSample(modifier: Modifier = Modifier.fillMaxWidth()) {
OutlinedButton(
onClick = { /* Do something */ },
@@ -186,7 +182,7 @@
secondaryLabel = { Text("Secondary label") },
icon = {
Icon(
- Icons.Filled.Favorite,
+ painter = painterResource(R.drawable.ic_favorite_rounded),
contentDescription = "Favorite icon",
modifier = Modifier.size(ButtonDefaults.IconSize)
)
@@ -207,23 +203,6 @@
@Sampled
@Composable
-fun ChildButtonWithOnLongClickSample(
- onClickHandler: () -> Unit,
- onLongClickHandler: () -> Unit,
- modifier: Modifier = Modifier.fillMaxWidth()
-) {
- ChildButton(
- onClick = onClickHandler,
- onLongClick = onLongClickHandler,
- onLongClickLabel = "Long click",
- label = { Text("Child Button") },
- secondaryLabel = { Text("with long click") },
- modifier = modifier,
- )
-}
-
-@Sampled
-@Composable
fun ChildButtonSample(modifier: Modifier = Modifier.fillMaxWidth()) {
ChildButton(
onClick = { /* Do something */ },
@@ -231,7 +210,7 @@
secondaryLabel = { Text("Secondary label") },
icon = {
Icon(
- Icons.Filled.Favorite,
+ painter = painterResource(R.drawable.ic_favorite_rounded),
contentDescription = "Favorite icon",
modifier = Modifier.size(ButtonDefaults.IconSize)
)
@@ -247,7 +226,7 @@
onClick = { /* Do something */ },
icon = {
Icon(
- Icons.Filled.Favorite,
+ painter = painterResource(R.drawable.ic_favorite_rounded),
contentDescription = "Favorite icon",
modifier = Modifier.size(ButtonDefaults.SmallIconSize)
)
@@ -281,7 +260,7 @@
onClick = { /* Do something */ },
icon = {
Icon(
- Icons.Filled.Favorite,
+ painter = painterResource(R.drawable.ic_favorite_rounded),
contentDescription = "Favorite icon",
modifier = Modifier.size(ButtonDefaults.SmallIconSize)
)
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ProgressIndicatorSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ProgressIndicatorSample.kt
index 4af6a52..3ba6437 100644
--- a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ProgressIndicatorSample.kt
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ProgressIndicatorSample.kt
@@ -61,11 +61,6 @@
progress = { 0.25f },
startAngle = 120f,
endAngle = 60f,
- colors =
- ProgressIndicatorDefaults.colors(
- indicatorColor = Color.Green,
- trackColor = Color.Green.copy(alpha = 0.5f)
- )
)
}
}
@@ -171,11 +166,6 @@
SegmentedCircularProgressIndicator(
segmentCount = 5,
progress = { 0.5f },
- colors =
- ProgressIndicatorDefaults.colors(
- indicatorColor = Color.Green,
- trackColor = Color.Green.copy(alpha = 0.5f)
- )
)
}
}
@@ -192,11 +182,18 @@
SegmentedCircularProgressIndicator(
segmentCount = 5,
completed = { it % 2 != 0 },
- colors =
- ProgressIndicatorDefaults.colors(
- indicatorColor = Color.Green,
- trackColor = Color.Green.copy(alpha = 0.5f)
- )
+ )
+ }
+}
+
+@Sampled
+@Composable
+fun SmallSegmentedProgressIndicatorSample() {
+ Box(modifier = Modifier.fillMaxSize()) {
+ SegmentedCircularProgressIndicator(
+ segmentCount = 8,
+ completed = { it % 2 != 0 },
+ modifier = Modifier.align(Alignment.Center).size(80.dp)
)
}
}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/Icons.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/icons/SampleIcons.kt
similarity index 70%
rename from wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/Icons.kt
rename to wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/icons/SampleIcons.kt
index b791a52..99bf58a 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/Icons.kt
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/icons/SampleIcons.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 The Android Open Source Project
+ * 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.
@@ -14,41 +14,39 @@
* limitations under the License.
*/
-package androidx.wear.compose.material3.demos
+package androidx.wear.compose.material3.samples.icons
import androidx.compose.foundation.layout.size
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.AccountCircle
-import androidx.compose.material.icons.filled.Check
-import androidx.compose.material.icons.filled.Favorite
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import androidx.wear.compose.material3.ButtonDefaults
import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.samples.R
@Composable
-internal fun StandardIcon(size: Dp) {
+fun FavoriteIcon(size: Dp) {
Icon(
- Icons.Filled.Favorite,
+ painter = painterResource(R.drawable.ic_favorite_rounded),
contentDescription = "Favorite icon",
modifier = Modifier.size(size)
)
}
@Composable
-internal fun AvatarIcon() {
+fun AvatarIcon() {
Icon(
- Icons.Filled.AccountCircle,
+ painter = painterResource(R.drawable.ic_account_circle),
contentDescription = "Account",
modifier = Modifier.size(ButtonDefaults.LargeIconSize)
)
}
@Composable
-internal fun CheckIcon() {
+fun CheckIcon() {
Icon(
- Icons.Filled.Check,
+ painter = painterResource(R.drawable.ic_check_rounded),
contentDescription = "Check",
modifier = Modifier.size(ButtonDefaults.IconSize)
)
diff --git a/wear/compose/compose-material3/samples/src/main/res/drawable/ic_account_circle.xml b/wear/compose/compose-material3/samples/src/main/res/drawable/ic_account_circle.xml
new file mode 100644
index 0000000..21441c1
--- /dev/null
+++ b/wear/compose/compose-material3/samples/src/main/res/drawable/ic_account_circle.xml
@@ -0,0 +1,25 @@
+<!--
+ 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M5.85,17.1C6.7,16.45 7.65,15.942 8.7,15.575C9.75,15.192 10.85,15 12,15C13.15,15 14.25,15.192 15.3,15.575C16.35,15.942 17.3,16.45 18.15,17.1C18.733,16.417 19.183,15.642 19.5,14.775C19.833,13.908 20,12.983 20,12C20,9.783 19.217,7.9 17.65,6.35C16.1,4.783 14.217,4 12,4C9.783,4 7.892,4.783 6.325,6.35C4.775,7.9 4,9.783 4,12C4,12.983 4.158,13.908 4.475,14.775C4.808,15.642 5.267,16.417 5.85,17.1ZM12,13C11.017,13 10.183,12.667 9.5,12C8.833,11.317 8.5,10.483 8.5,9.5C8.5,8.517 8.833,7.692 9.5,7.025C10.183,6.342 11.017,6 12,6C12.983,6 13.808,6.342 14.475,7.025C15.158,7.692 15.5,8.517 15.5,9.5C15.5,10.483 15.158,11.317 14.475,12C13.808,12.667 12.983,13 12,13ZM12,22C10.617,22 9.317,21.742 8.1,21.225C6.883,20.692 5.825,19.975 4.925,19.075C4.025,18.175 3.308,17.117 2.775,15.9C2.258,14.683 2,13.383 2,12C2,10.617 2.258,9.317 2.775,8.1C3.308,6.883 4.025,5.825 4.925,4.925C5.825,4.025 6.883,3.317 8.1,2.8C9.317,2.267 10.617,2 12,2C13.383,2 14.683,2.267 15.9,2.8C17.117,3.317 18.175,4.025 19.075,4.925C19.975,5.825 20.683,6.883 21.2,8.1C21.733,9.317 22,10.617 22,12C22,13.383 21.733,14.683 21.2,15.9C20.683,17.117 19.975,18.175 19.075,19.075C18.175,19.975 17.117,20.692 15.9,21.225C14.683,21.742 13.383,22 12,22Z"
+ android:fillColor="#FFFFFF"/>
+</vector>
diff --git a/wear/compose/compose-material3/samples/src/main/res/drawable/ic_check_rounded.xml b/wear/compose/compose-material3/samples/src/main/res/drawable/ic_check_rounded.xml
new file mode 100644
index 0000000..19c9634
--- /dev/null
+++ b/wear/compose/compose-material3/samples/src/main/res/drawable/ic_check_rounded.xml
@@ -0,0 +1,25 @@
+<!--
+ 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M9.55,15.15L18.025,6.675C18.225,6.475 18.458,6.375 18.725,6.375C18.992,6.375 19.225,6.475 19.425,6.675C19.625,6.875 19.725,7.117 19.725,7.4C19.725,7.667 19.625,7.9 19.425,8.1L10.25,17.3C10.05,17.5 9.817,17.6 9.55,17.6C9.283,17.6 9.05,17.5 8.85,17.3L4.55,13C4.35,12.8 4.25,12.567 4.25,12.3C4.267,12.017 4.375,11.775 4.575,11.575C4.775,11.375 5.008,11.275 5.275,11.275C5.558,11.275 5.8,11.375 6,11.575L9.55,15.15Z"
+ android:fillColor="#FFFFFF"/>
+</vector>
diff --git a/wear/compose/compose-material3/samples/src/main/res/drawable/ic_favorite_rounded.xml b/wear/compose/compose-material3/samples/src/main/res/drawable/ic_favorite_rounded.xml
new file mode 100644
index 0000000..d52b7e5
--- /dev/null
+++ b/wear/compose/compose-material3/samples/src/main/res/drawable/ic_favorite_rounded.xml
@@ -0,0 +1,25 @@
+<!--
+ 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M12,20.325C11.767,20.325 11.525,20.283 11.275,20.2C11.042,20.117 10.833,19.983 10.65,19.8L8.925,18.225C7.158,16.608 5.558,15.008 4.125,13.425C2.708,11.825 2,10.067 2,8.15C2,6.583 2.525,5.275 3.575,4.225C4.625,3.175 5.933,2.65 7.5,2.65C8.383,2.65 9.217,2.842 10,3.225C10.783,3.592 11.45,4.1 12,4.75C12.55,4.1 13.217,3.592 14,3.225C14.783,2.842 15.617,2.65 16.5,2.65C18.067,2.65 19.375,3.175 20.425,4.225C21.475,5.275 22,6.583 22,8.15C22,10.067 21.292,11.825 19.875,13.425C18.458,15.025 16.85,16.633 15.05,18.25L13.35,19.8C13.167,19.983 12.95,20.117 12.7,20.2C12.467,20.283 12.233,20.325 12,20.325Z"
+ android:fillColor="#FFFFFF"/>
+</vector>
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogScreenshotTest.kt
index e049442..7074d90 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogScreenshotTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogScreenshotTest.kt
@@ -64,12 +64,12 @@
testName = testName,
screenshotRule = screenshotRule,
showIcon = false,
- showText = false,
showContent = false,
showTwoButtons = false,
scrollToBottom = false,
screenSize = screenSize,
- titleText = "Network error"
+ titleText = "Network error",
+ messageText = null
)
@Test
@@ -78,12 +78,12 @@
testName = testName,
screenshotRule = screenshotRule,
showIcon = false,
- showText = false,
showContent = false,
showTwoButtons = true,
scrollToBottom = false,
screenSize = screenSize,
- titleText = "Network error"
+ titleText = "Network error",
+ messageText = null
)
@Test
@@ -92,11 +92,11 @@
testName = testName,
screenshotRule = screenshotRule,
showIcon = false,
- showText = false,
showContent = false,
showTwoButtons = false,
scrollToBottom = false,
screenSize = screenSize,
+ messageText = null,
)
@Test
@@ -105,11 +105,11 @@
testName = testName,
screenshotRule = screenshotRule,
showIcon = false,
- showText = false,
showContent = false,
showTwoButtons = true,
scrollToBottom = false,
- screenSize = screenSize
+ screenSize = screenSize,
+ messageText = null
)
@Test
@@ -118,11 +118,11 @@
testName = testName,
screenshotRule = screenshotRule,
showIcon = true,
- showText = false,
showContent = false,
showTwoButtons = false,
scrollToBottom = false,
- screenSize = screenSize
+ screenSize = screenSize,
+ messageText = null
)
}
@@ -132,11 +132,11 @@
testName = testName,
screenshotRule = screenshotRule,
showIcon = true,
- showText = false,
showContent = false,
showTwoButtons = true,
scrollToBottom = false,
- screenSize = screenSize
+ screenSize = screenSize,
+ messageText = null
)
}
@@ -146,7 +146,6 @@
testName = testName,
screenshotRule = screenshotRule,
showIcon = true,
- showText = true,
showContent = false,
showTwoButtons = false,
scrollToBottom = false,
@@ -162,7 +161,6 @@
testName = testName,
screenshotRule = screenshotRule,
showIcon = true,
- showText = true,
showContent = true,
showTwoButtons = false,
scrollToBottom = false,
@@ -178,7 +176,6 @@
testName = testName,
screenshotRule = screenshotRule,
showIcon = true,
- showText = true,
showContent = true,
showTwoButtons = false,
scrollToBottom = true,
@@ -194,7 +191,6 @@
testName = testName,
screenshotRule = screenshotRule,
showIcon = true,
- showText = true,
showContent = true,
showTwoButtons = true,
scrollToBottom = true,
@@ -202,15 +198,43 @@
)
}
+ @Test
+ fun alert_title_longMessageText_bottomButton(@TestParameter screenSize: ScreenSize) {
+ rule.verifyAlertDialogScreenshot(
+ testName = testName,
+ screenshotRule = screenshotRule,
+ showIcon = false,
+ showContent = false,
+ showTwoButtons = false,
+ scrollToBottom = false,
+ screenSize = screenSize,
+ messageText = longMessageText
+ )
+ }
+
+ @Test
+ fun alert_title_longMessageText_confirmDismissButtons(@TestParameter screenSize: ScreenSize) {
+ rule.verifyAlertDialogScreenshot(
+ testName = testName,
+ screenshotRule = screenshotRule,
+ showIcon = false,
+ showContent = false,
+ showTwoButtons = true,
+ scrollToBottom = false,
+ screenSize = screenSize,
+ messageText = longMessageText
+ )
+ }
+
private fun ComposeContentTestRule.verifyAlertDialogScreenshot(
testName: TestName,
screenshotRule: AndroidXScreenshotTestRule,
showIcon: Boolean,
- showText: Boolean,
showContent: Boolean,
showTwoButtons: Boolean,
scrollToBottom: Boolean,
screenSize: ScreenSize,
+ messageText: String? = "Your battery is low. Turn on battery saver.",
titleText: String = "Mobile network is not currently available"
) {
setContentWithTheme() {
@@ -239,8 +263,8 @@
} else null,
showTwoButtons = showTwoButtons,
text =
- if (showText) {
- { Text("Your battery is low. Turn on battery saver.") }
+ if (messageText != null) {
+ { Text(messageText) }
} else null,
content =
if (showContent) {
@@ -308,3 +332,6 @@
)
}
}
+
+internal const val longMessageText =
+ "Allow Map to access your location even when you're not using the app? Your location is used to automatically map places to activities."
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogTest.kt
index 6e59369..63cb2df 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogTest.kt
@@ -263,7 +263,7 @@
var expectedContentColor: Color = Color.Unspecified
var expectedTextStyle: TextStyle = TextStyle.Default
var expectedTextAlign: TextAlign? = null
- var expectedTextMaxLines: Int = 0
+ var expectedTextMaxLines = 0
var actualContentColor: Color = Color.Unspecified
var actualTextStyle: TextStyle = TextStyle.Default
@@ -281,8 +281,8 @@
Text("Title")
actualContentColor = LocalContentColor.current
actualTextStyle = LocalTextStyle.current
- actualTextAlign = LocalTextAlign.current
- actualTextMaxLines = LocalTextMaxLines.current
+ actualTextAlign = LocalTextConfiguration.current.textAlign
+ actualTextMaxLines = LocalTextConfiguration.current.maxLines
},
bottomButton = {},
onDismissRequest = {},
@@ -317,7 +317,7 @@
Text("Text")
actualContentColor = LocalContentColor.current
actualTextStyle = LocalTextStyle.current
- actualTextAlign = LocalTextAlign.current
+ actualTextAlign = LocalTextConfiguration.current.textAlign
},
bottomButton = {},
onDismissRequest = {},
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonScreenshotTest.kt
index 4b23eab..5baf0a4 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonScreenshotTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonScreenshotTest.kt
@@ -21,9 +21,13 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.AccountCircle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.platform.LocalLayoutDirection
@@ -32,6 +36,7 @@
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
@@ -43,11 +48,11 @@
import androidx.wear.compose.material3.ChildButton
import androidx.wear.compose.material3.CompactButton
import androidx.wear.compose.material3.FilledTonalButton
+import androidx.wear.compose.material3.Icon
import androidx.wear.compose.material3.MaterialTheme
import androidx.wear.compose.material3.OutlinedButton
import androidx.wear.compose.material3.SCREENSHOT_GOLDEN_PATH
import androidx.wear.compose.material3.TEST_TAG
-import androidx.wear.compose.material3.TestIcon
import androidx.wear.compose.material3.Text
import androidx.wear.compose.material3.setContentWithTheme
import org.junit.Rule
@@ -65,9 +70,25 @@
@get:Rule val testName = TestName()
- @Test fun button_enabled() = verifyScreenshot() { BaseButton() }
+ @Test fun button_enabled() = verifyScreenshot { BaseButton() }
- @Test fun button_disabled() = verifyScreenshot() { BaseButton(enabled = false) }
+ @Test fun button_disabled() = verifyScreenshot { BaseButton(enabled = false) }
+
+ @Test
+ fun button_default_alignment() = verifyScreenshot {
+ // Uses the base Button overload, should be vertically centered by default
+ Button(onClick = {}, modifier = Modifier.fillMaxWidth().testTag(TEST_TAG)) {
+ Text("Button")
+ }
+ }
+
+ @Test
+ fun button_top_alignment() = verifyScreenshot {
+ // Uses RowScope to override the default vertical alignment to be top
+ Button(onClick = {}, modifier = Modifier.fillMaxWidth().testTag(TEST_TAG)) {
+ Text("Button", modifier = Modifier.align(Alignment.Top))
+ }
+ }
@Test
fun three_slot_button_ltr() =
@@ -111,7 +132,31 @@
onClick = {},
modifier = Modifier.fillMaxWidth().testTag(TEST_TAG),
label = { Text("Label only", modifier = Modifier.fillMaxWidth()) },
- icon = { TestIcon() },
+ icon = { ButtonIcon(size = ButtonDefaults.IconSize) },
+ )
+ }
+
+ @Test
+ fun button_large_icon() = verifyScreenshot {
+ Button(
+ onClick = {},
+ modifier = Modifier.fillMaxWidth().testTag(TEST_TAG),
+ label = { Text("Label", modifier = Modifier.fillMaxWidth()) },
+ secondaryLabel = { Text("Secondary label", modifier = Modifier.fillMaxWidth()) },
+ icon = { ButtonIcon(size = ButtonDefaults.LargeIconSize) },
+ contentPadding = ButtonDefaults.ButtonWithLargeIconContentPadding
+ )
+ }
+
+ @Test
+ fun button_extra_large_icon() = verifyScreenshot {
+ Button(
+ onClick = {},
+ modifier = Modifier.fillMaxWidth().testTag(TEST_TAG),
+ label = { Text("Label", modifier = Modifier.fillMaxWidth()) },
+ secondaryLabel = { Text("Secondary label", modifier = Modifier.fillMaxWidth()) },
+ icon = { ButtonIcon(size = ButtonDefaults.ExtraLargeIconSize) },
+ contentPadding = ButtonDefaults.ButtonWithExtraLargeIconContentPadding
)
}
@@ -130,7 +175,7 @@
onClick = {},
modifier = Modifier.fillMaxWidth().testTag(TEST_TAG),
label = { Text("Label only", modifier = Modifier.fillMaxWidth()) },
- icon = { TestIcon() },
+ icon = { ButtonIcon(size = ButtonDefaults.IconSize) },
)
}
@@ -149,7 +194,7 @@
onClick = {},
modifier = Modifier.fillMaxWidth().testTag(TEST_TAG),
label = { Text("Label only", modifier = Modifier.fillMaxWidth()) },
- icon = { TestIcon() },
+ icon = { ButtonIcon(size = ButtonDefaults.IconSize) },
)
}
@@ -168,7 +213,7 @@
onClick = {},
modifier = Modifier.fillMaxWidth().testTag(TEST_TAG),
label = { Text("Label only", modifier = Modifier.fillMaxWidth()) },
- icon = { TestIcon() },
+ icon = { ButtonIcon(size = ButtonDefaults.IconSize) },
)
}
@@ -191,7 +236,7 @@
onClick = {},
modifier = Modifier.fillMaxWidth().testTag(TEST_TAG),
label = { Text("Label only", modifier = Modifier.fillMaxWidth()) },
- icon = { TestIcon() },
+ icon = { ButtonIcon(size = ButtonDefaults.SmallIconSize) },
)
}
@@ -209,7 +254,7 @@
onClick = {},
label = { Text("Three Slot Button") },
secondaryLabel = { Text("Secondary Label") },
- icon = { TestIcon() },
+ icon = { ButtonIcon(size = ButtonDefaults.IconSize) },
modifier = Modifier.testTag(TEST_TAG)
)
}
@@ -233,7 +278,7 @@
backgroundImagePainter = painterResource(R.drawable.backgroundimage1),
forcedSize = size
),
- icon = { TestIcon() },
+ icon = { ButtonIcon(size = ButtonDefaults.SmallIconSize) },
modifier = Modifier.testTag(TEST_TAG)
)
}
@@ -243,7 +288,7 @@
CompactButton(
onClick = {},
label = { Text("Compact Button") },
- icon = { TestIcon() },
+ icon = { ButtonIcon(size = ButtonDefaults.SmallIconSize) },
enabled = enabled,
modifier = Modifier.testTag(TEST_TAG)
)
@@ -269,4 +314,18 @@
.captureToImage()
.assertAgainstGolden(screenshotRule, testName.methodName)
}
+
+ @Composable
+ private fun ButtonIcon(
+ size: Dp,
+ modifier: Modifier = Modifier,
+ iconLabel: String = "ButtonIcon",
+ ) {
+ val testImage = Icons.Outlined.AccountCircle
+ Icon(
+ imageVector = testImage,
+ contentDescription = iconLabel,
+ modifier = modifier.testTag(iconLabel).size(size)
+ )
+ }
}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonTest.kt
index ddc439a..2bc38ef 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonTest.kt
@@ -854,8 +854,10 @@
rule.setContentWithTheme {
Button(
onClick = {},
- label = { labelOverflow = LocalTextOverflow.current },
- secondaryLabel = { secondaryLabelOverflow = LocalTextOverflow.current },
+ label = { labelOverflow = LocalTextConfiguration.current.overflow },
+ secondaryLabel = {
+ secondaryLabelOverflow = LocalTextConfiguration.current.overflow
+ },
)
}
@@ -871,8 +873,10 @@
rule.setContentWithTheme {
Button(
onClick = {},
- label = { labelMaxLines = LocalTextMaxLines.current },
- secondaryLabel = { secondaryLabelMaxLines = LocalTextMaxLines.current },
+ label = { labelMaxLines = LocalTextConfiguration.current.maxLines },
+ secondaryLabel = {
+ secondaryLabelMaxLines = LocalTextConfiguration.current.maxLines
+ },
)
}
@@ -888,8 +892,10 @@
rule.setContentWithTheme {
Button(
onClick = {},
- label = { labelAlignment = LocalTextAlign.current },
- secondaryLabel = { secondaryLabelAlignment = LocalTextAlign.current },
+ label = { labelAlignment = LocalTextConfiguration.current.textAlign },
+ secondaryLabel = {
+ secondaryLabelAlignment = LocalTextConfiguration.current.textAlign
+ },
)
}
@@ -904,7 +910,7 @@
rule.setContentWithTheme {
Button(
onClick = {},
- label = { labelAlignment = LocalTextAlign.current },
+ label = { labelAlignment = LocalTextConfiguration.current.textAlign },
)
}
@@ -919,8 +925,10 @@
rule.setContentWithTheme {
FilledTonalButton(
onClick = {},
- label = { labelOverflow = LocalTextOverflow.current },
- secondaryLabel = { secondaryLabelOverflow = LocalTextOverflow.current },
+ label = { labelOverflow = LocalTextConfiguration.current.overflow },
+ secondaryLabel = {
+ secondaryLabelOverflow = LocalTextConfiguration.current.overflow
+ },
)
}
@@ -936,8 +944,10 @@
rule.setContentWithTheme {
FilledTonalButton(
onClick = {},
- label = { labelMaxLines = LocalTextMaxLines.current },
- secondaryLabel = { secondaryLabelMaxLines = LocalTextMaxLines.current },
+ label = { labelMaxLines = LocalTextConfiguration.current.maxLines },
+ secondaryLabel = {
+ secondaryLabelMaxLines = LocalTextConfiguration.current.maxLines
+ },
)
}
@@ -953,8 +963,10 @@
rule.setContentWithTheme {
FilledTonalButton(
onClick = {},
- label = { labelAlignment = LocalTextAlign.current },
- secondaryLabel = { secondaryLabelAlignment = LocalTextAlign.current },
+ label = { labelAlignment = LocalTextConfiguration.current.textAlign },
+ secondaryLabel = {
+ secondaryLabelAlignment = LocalTextConfiguration.current.textAlign
+ },
)
}
@@ -969,7 +981,7 @@
rule.setContentWithTheme {
FilledTonalButton(
onClick = {},
- label = { labelAlignment = LocalTextAlign.current },
+ label = { labelAlignment = LocalTextConfiguration.current.textAlign },
)
}
@@ -984,8 +996,10 @@
rule.setContentWithTheme {
OutlinedButton(
onClick = {},
- label = { labelOverflow = LocalTextOverflow.current },
- secondaryLabel = { secondaryLabelOverflow = LocalTextOverflow.current },
+ label = { labelOverflow = LocalTextConfiguration.current.overflow },
+ secondaryLabel = {
+ secondaryLabelOverflow = LocalTextConfiguration.current.overflow
+ },
)
}
@@ -1001,8 +1015,10 @@
rule.setContentWithTheme {
OutlinedButton(
onClick = {},
- label = { labelMaxLines = LocalTextMaxLines.current },
- secondaryLabel = { secondaryLabelMaxLines = LocalTextMaxLines.current },
+ label = { labelMaxLines = LocalTextConfiguration.current.maxLines },
+ secondaryLabel = {
+ secondaryLabelMaxLines = LocalTextConfiguration.current.maxLines
+ },
)
}
@@ -1018,8 +1034,10 @@
rule.setContentWithTheme {
OutlinedButton(
onClick = {},
- label = { labelAlignment = LocalTextAlign.current },
- secondaryLabel = { secondaryLabelAlignment = LocalTextAlign.current },
+ label = { labelAlignment = LocalTextConfiguration.current.textAlign },
+ secondaryLabel = {
+ secondaryLabelAlignment = LocalTextConfiguration.current.textAlign
+ },
)
}
@@ -1034,7 +1052,7 @@
rule.setContentWithTheme {
OutlinedButton(
onClick = {},
- label = { labelAlignment = LocalTextAlign.current },
+ label = { labelAlignment = LocalTextConfiguration.current.textAlign },
)
}
@@ -1049,8 +1067,10 @@
rule.setContentWithTheme {
ChildButton(
onClick = {},
- label = { labelOverflow = LocalTextOverflow.current },
- secondaryLabel = { secondaryLabelOverflow = LocalTextOverflow.current },
+ label = { labelOverflow = LocalTextConfiguration.current.overflow },
+ secondaryLabel = {
+ secondaryLabelOverflow = LocalTextConfiguration.current.overflow
+ },
)
}
@@ -1066,8 +1086,10 @@
rule.setContentWithTheme {
ChildButton(
onClick = {},
- label = { labelMaxLines = LocalTextMaxLines.current },
- secondaryLabel = { secondaryLabelMaxLines = LocalTextMaxLines.current },
+ label = { labelMaxLines = LocalTextConfiguration.current.maxLines },
+ secondaryLabel = {
+ secondaryLabelMaxLines = LocalTextConfiguration.current.maxLines
+ },
)
}
@@ -1083,8 +1105,10 @@
rule.setContentWithTheme {
ChildButton(
onClick = {},
- label = { labelAlignment = LocalTextAlign.current },
- secondaryLabel = { secondaryLabelAlignment = LocalTextAlign.current },
+ label = { labelAlignment = LocalTextConfiguration.current.textAlign },
+ secondaryLabel = {
+ secondaryLabelAlignment = LocalTextConfiguration.current.textAlign
+ },
)
}
@@ -1099,7 +1123,7 @@
rule.setContentWithTheme {
ChildButton(
onClick = {},
- label = { labelAlignment = LocalTextAlign.current },
+ label = { labelAlignment = LocalTextConfiguration.current.textAlign },
)
}
@@ -1113,7 +1137,7 @@
rule.setContentWithTheme {
CompactButton(
onClick = {},
- label = { labelOverflow = LocalTextOverflow.current },
+ label = { labelOverflow = LocalTextConfiguration.current.overflow },
)
}
@@ -1127,7 +1151,7 @@
rule.setContentWithTheme {
CompactButton(
onClick = {},
- label = { labelMaxLines = LocalTextMaxLines.current },
+ label = { labelMaxLines = LocalTextConfiguration.current.maxLines },
)
}
@@ -1141,7 +1165,7 @@
rule.setContentWithTheme {
CompactButton(
onClick = {},
- label = { labelAlignment = LocalTextAlign.current },
+ label = { labelAlignment = LocalTextConfiguration.current.textAlign },
icon = {},
)
}
@@ -1156,7 +1180,7 @@
rule.setContentWithTheme {
CompactButton(
onClick = {},
- label = { labelAlignment = LocalTextAlign.current },
+ label = { labelAlignment = LocalTextConfiguration.current.textAlign },
)
}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/CheckboxButtonTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/CheckboxButtonTest.kt
index 9f3afc7..ff443c2 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/CheckboxButtonTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/CheckboxButtonTest.kt
@@ -440,8 +440,10 @@
CheckboxButtonWithDefaults(
checked = true,
onCheckedChange = {},
- label = { labelTextAlign = LocalTextAlign.current },
- secondaryLabel = { secondaryLabelTextAlign = LocalTextAlign.current },
+ label = { labelTextAlign = LocalTextConfiguration.current.textAlign },
+ secondaryLabel = {
+ secondaryLabelTextAlign = LocalTextConfiguration.current.textAlign
+ },
)
}
@@ -458,8 +460,10 @@
SplitCheckboxButtonWithDefaults(
checked = true,
onCheckedChange = {},
- label = { labelTextAlign = LocalTextAlign.current },
- secondaryLabel = { secondaryLabelTextAlign = LocalTextAlign.current },
+ label = { labelTextAlign = LocalTextConfiguration.current.textAlign },
+ secondaryLabel = {
+ secondaryLabelTextAlign = LocalTextConfiguration.current.textAlign
+ },
)
}
@@ -476,8 +480,10 @@
CheckboxButtonWithDefaults(
checked = true,
onCheckedChange = {},
- label = { labelOverflow = LocalTextOverflow.current },
- secondaryLabel = { secondaryLabelOverflow = LocalTextOverflow.current },
+ label = { labelOverflow = LocalTextConfiguration.current.overflow },
+ secondaryLabel = {
+ secondaryLabelOverflow = LocalTextConfiguration.current.overflow
+ },
)
}
@@ -494,8 +500,10 @@
SplitCheckboxButtonWithDefaults(
checked = true,
onCheckedChange = {},
- label = { labelOverflow = LocalTextOverflow.current },
- secondaryLabel = { secondaryLabelOverflow = LocalTextOverflow.current },
+ label = { labelOverflow = LocalTextConfiguration.current.overflow },
+ secondaryLabel = {
+ secondaryLabelOverflow = LocalTextConfiguration.current.overflow
+ },
)
}
@@ -512,8 +520,10 @@
CheckboxButtonWithDefaults(
checked = true,
onCheckedChange = {},
- label = { labelMaxLines = LocalTextMaxLines.current },
- secondaryLabel = { secondaryLabelMaxLines = LocalTextMaxLines.current },
+ label = { labelMaxLines = LocalTextConfiguration.current.maxLines },
+ secondaryLabel = {
+ secondaryLabelMaxLines = LocalTextConfiguration.current.maxLines
+ },
)
}
@@ -530,8 +540,10 @@
SplitCheckboxButtonWithDefaults(
checked = true,
onCheckedChange = {},
- label = { labelMaxLines = LocalTextMaxLines.current },
- secondaryLabel = { secondaryLabelMaxLines = LocalTextMaxLines.current },
+ label = { labelMaxLines = LocalTextConfiguration.current.maxLines },
+ secondaryLabel = {
+ secondaryLabelMaxLines = LocalTextConfiguration.current.maxLines
+ },
)
}
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 16b3d25..4b6a973 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
@@ -479,8 +479,10 @@
rule.setContentWithTheme {
RadioButtonWithDefaults(
selected = true,
- label = { labelTextAlign = LocalTextAlign.current },
- secondaryLabel = { secondaryLabelTextAlign = LocalTextAlign.current },
+ label = { labelTextAlign = LocalTextConfiguration.current.textAlign },
+ secondaryLabel = {
+ secondaryLabelTextAlign = LocalTextConfiguration.current.textAlign
+ },
)
}
@@ -496,8 +498,10 @@
rule.setContentWithTheme {
SplitRadioButtonWithDefaults(
selected = true,
- label = { labelTextAlign = LocalTextAlign.current },
- secondaryLabel = { secondaryLabelTextAlign = LocalTextAlign.current },
+ label = { labelTextAlign = LocalTextConfiguration.current.textAlign },
+ secondaryLabel = {
+ secondaryLabelTextAlign = LocalTextConfiguration.current.textAlign
+ },
)
}
@@ -513,8 +517,10 @@
rule.setContentWithTheme {
RadioButtonWithDefaults(
selected = true,
- label = { labelOverflow = LocalTextOverflow.current },
- secondaryLabel = { secondaryLabelOverflow = LocalTextOverflow.current },
+ label = { labelOverflow = LocalTextConfiguration.current.overflow },
+ secondaryLabel = {
+ secondaryLabelOverflow = LocalTextConfiguration.current.overflow
+ },
)
}
@@ -530,8 +536,10 @@
rule.setContentWithTheme {
SplitRadioButtonWithDefaults(
selected = true,
- label = { labelOverflow = LocalTextOverflow.current },
- secondaryLabel = { secondaryLabelOverflow = LocalTextOverflow.current },
+ label = { labelOverflow = LocalTextConfiguration.current.overflow },
+ secondaryLabel = {
+ secondaryLabelOverflow = LocalTextConfiguration.current.overflow
+ },
)
}
@@ -547,8 +555,10 @@
rule.setContentWithTheme {
RadioButtonWithDefaults(
selected = true,
- label = { labelMaxLines = LocalTextMaxLines.current },
- secondaryLabel = { secondaryLabelMaxLines = LocalTextMaxLines.current },
+ label = { labelMaxLines = LocalTextConfiguration.current.maxLines },
+ secondaryLabel = {
+ secondaryLabelMaxLines = LocalTextConfiguration.current.maxLines
+ },
)
}
@@ -564,8 +574,10 @@
rule.setContentWithTheme {
SplitRadioButtonWithDefaults(
selected = true,
- label = { labelMaxLines = LocalTextMaxLines.current },
- secondaryLabel = { secondaryLabelMaxLines = LocalTextMaxLines.current },
+ label = { labelMaxLines = LocalTextConfiguration.current.maxLines },
+ secondaryLabel = {
+ secondaryLabelMaxLines = LocalTextConfiguration.current.maxLines
+ },
)
}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SwitchButtonTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SwitchButtonTest.kt
index e79d354..7300690 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SwitchButtonTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SwitchButtonTest.kt
@@ -436,8 +436,10 @@
SwitchButtonWithDefaults(
checked = true,
onCheckedChange = {},
- label = { labelTextAlign = LocalTextAlign.current },
- secondaryLabel = { secondaryLabelTextAlign = LocalTextAlign.current },
+ label = { labelTextAlign = LocalTextConfiguration.current.textAlign },
+ secondaryLabel = {
+ secondaryLabelTextAlign = LocalTextConfiguration.current.textAlign
+ },
)
}
@@ -454,8 +456,10 @@
SplitSwitchButtonWithDefaults(
checked = true,
onCheckedChange = {},
- label = { labelTextAlign = LocalTextAlign.current },
- secondaryLabel = { secondaryLabelTextAlign = LocalTextAlign.current },
+ label = { labelTextAlign = LocalTextConfiguration.current.textAlign },
+ secondaryLabel = {
+ secondaryLabelTextAlign = LocalTextConfiguration.current.textAlign
+ },
)
}
@@ -472,8 +476,10 @@
SwitchButtonWithDefaults(
checked = true,
onCheckedChange = {},
- label = { labelOverflow = LocalTextOverflow.current },
- secondaryLabel = { secondaryLabelOverflow = LocalTextOverflow.current },
+ label = { labelOverflow = LocalTextConfiguration.current.overflow },
+ secondaryLabel = {
+ secondaryLabelOverflow = LocalTextConfiguration.current.overflow
+ },
)
}
@@ -490,8 +496,10 @@
SplitSwitchButtonWithDefaults(
checked = true,
onCheckedChange = {},
- label = { labelOverflow = LocalTextOverflow.current },
- secondaryLabel = { secondaryLabelOverflow = LocalTextOverflow.current },
+ label = { labelOverflow = LocalTextConfiguration.current.overflow },
+ secondaryLabel = {
+ secondaryLabelOverflow = LocalTextConfiguration.current.overflow
+ },
)
}
@@ -508,8 +516,10 @@
SwitchButtonWithDefaults(
checked = true,
onCheckedChange = {},
- label = { labelMaxLines = LocalTextMaxLines.current },
- secondaryLabel = { secondaryLabelMaxLines = LocalTextMaxLines.current },
+ label = { labelMaxLines = LocalTextConfiguration.current.maxLines },
+ secondaryLabel = {
+ secondaryLabelMaxLines = LocalTextConfiguration.current.maxLines
+ },
)
}
@@ -526,8 +536,10 @@
SplitSwitchButtonWithDefaults(
checked = true,
onCheckedChange = {},
- label = { labelMaxLines = LocalTextMaxLines.current },
- secondaryLabel = { secondaryLabelMaxLines = LocalTextMaxLines.current },
+ label = { labelMaxLines = LocalTextConfiguration.current.maxLines },
+ secondaryLabel = {
+ secondaryLabelMaxLines = LocalTextConfiguration.current.maxLines
+ },
)
}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextScreenshotTest.kt
index f4bcb12..738ce58 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextScreenshotTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextScreenshotTest.kt
@@ -36,11 +36,13 @@
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import androidx.test.screenshot.AndroidXScreenshotTestRule
-import androidx.wear.compose.material3.LocalTextAlign
+import androidx.wear.compose.material3.LocalTextConfiguration
import androidx.wear.compose.material3.MaterialTheme
import androidx.wear.compose.material3.SCREENSHOT_GOLDEN_PATH
import androidx.wear.compose.material3.TEST_TAG
import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.TextConfiguration
+import androidx.wear.compose.material3.TextConfigurationDefaults
import androidx.wear.compose.material3.setContentWithTheme
import org.junit.Rule
import org.junit.Test
@@ -62,12 +64,30 @@
@Test
fun text_align_follows_composition_local_center_alignment() = verifyScreenshot {
- CompositionLocalProvider(LocalTextAlign provides TextAlign.Center) { sampleText() }
+ CompositionLocalProvider(
+ LocalTextConfiguration provides
+ TextConfiguration(
+ textAlign = TextAlign.Center,
+ overflow = TextConfigurationDefaults.Overflow,
+ maxLines = TextConfigurationDefaults.MaxLines,
+ )
+ ) {
+ sampleText()
+ }
}
@Test
fun text_align_follows_composition_local_end_alignment() = verifyScreenshot {
- CompositionLocalProvider(LocalTextAlign provides TextAlign.End) { sampleText() }
+ CompositionLocalProvider(
+ LocalTextConfiguration provides
+ TextConfiguration(
+ textAlign = TextAlign.End,
+ overflow = TextConfigurationDefaults.Overflow,
+ maxLines = TextConfigurationDefaults.MaxLines,
+ )
+ ) {
+ sampleText()
+ }
}
@Composable
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextTest.kt
index 607d7d3e..b852a5b 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextTest.kt
@@ -266,7 +266,14 @@
"working otherwise it would not be a good test."
var result: TextLayoutResult? = null
rule.setContent {
- CompositionLocalProvider(LocalTextMaxLines provides maxLines) {
+ CompositionLocalProvider(
+ LocalTextConfiguration provides
+ TextConfiguration(
+ maxLines = maxLines,
+ textAlign = TextConfigurationDefaults.TextAlign,
+ overflow = TextConfigurationDefaults.Overflow,
+ )
+ ) {
Text(
text = text,
onTextLayout = { textLayoutResult -> result = textLayoutResult },
@@ -323,7 +330,14 @@
"working otherwise it would not be a good test."
var result: TextLayoutResult? = null
rule.setContent {
- CompositionLocalProvider(LocalTextOverflow provides TextOverflow.Ellipsis) {
+ CompositionLocalProvider(
+ LocalTextConfiguration provides
+ TextConfiguration(
+ overflow = TextOverflow.Ellipsis,
+ textAlign = TextConfigurationDefaults.TextAlign,
+ maxLines = TextConfigurationDefaults.MaxLines,
+ )
+ ) {
Text(
text = text,
maxLines = 1,
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AlertDialog.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AlertDialog.kt
index f0cc000..5c2bdd2 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AlertDialog.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AlertDialog.kt
@@ -41,6 +41,7 @@
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
@@ -338,7 +339,7 @@
alertButtonsParams: AlertButtonsParams,
content: (ScalingLazyListScope.() -> Unit)?
) {
- val state = rememberScalingLazyListState()
+ val state = rememberScalingLazyListState(initialCenterItemIndex = 0)
Dialog(
showDialog = show,
@@ -401,8 +402,12 @@
CompositionLocalProvider(
LocalContentColor provides MaterialTheme.colorScheme.onBackground,
LocalTextStyle provides MaterialTheme.typography.titleMedium,
- LocalTextAlign provides TextAlign.Center,
- LocalTextMaxLines provides AlertDialogDefaults.titleMaxLines,
+ LocalTextConfiguration provides
+ TextConfiguration(
+ textAlign = TextAlign.Center,
+ maxLines = AlertDialogDefaults.titleMaxLines,
+ overflow = TextOverflow.Ellipsis
+ ),
content = content
)
}
@@ -433,7 +438,12 @@
CompositionLocalProvider(
LocalContentColor provides MaterialTheme.colorScheme.onBackground,
LocalTextStyle provides MaterialTheme.typography.bodyMedium,
- LocalTextAlign provides TextAlign.Center,
+ LocalTextConfiguration provides
+ TextConfiguration(
+ textAlign = TextAlign.Center,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = TextConfigurationDefaults.MaxLines
+ ),
content = content
)
}
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 0cf422b..1fddec4 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
@@ -92,10 +92,6 @@
* Example of a [Button]:
*
* @sample androidx.wear.compose.material3.samples.SimpleButtonSample
- *
- * Example of a [Button] with onLongClick:
- *
- * @sample androidx.wear.compose.material3.samples.ButtonWithOnLongClickSample
* @param onClick Will be called when the user clicks the button
* @param modifier Modifier to be applied to the button
* @param onLongClick Called when this button is long clicked (long-pressed). When this callback is
@@ -175,10 +171,6 @@
* Example of a [FilledTonalButton]:
*
* @sample androidx.wear.compose.material3.samples.SimpleFilledTonalButtonSample
- *
- * Example of a [FilledTonalButton] with onLongClick:
- *
- * @sample androidx.wear.compose.material3.samples.FilledTonalButtonWithOnLongClickSample
* @param onClick Will be called when the user clicks the button
* @param modifier Modifier to be applied to the button
* @param onLongClick Called when this button is long clicked (long-pressed). When this callback is
@@ -257,10 +249,6 @@
* Example of an [OutlinedButton]:
*
* @sample androidx.wear.compose.material3.samples.SimpleOutlinedButtonSample
- *
- * Example of a [OutlinedButton] with onLongClick:
- *
- * @sample androidx.wear.compose.material3.samples.OutlinedButtonWithOnLongClickSample
* @param onClick Will be called when the user clicks the button
* @param modifier Modifier to be applied to the button
* @param onLongClick Called when this button is long clicked (long-pressed). When this callback is
@@ -339,10 +327,6 @@
* Example of a [ChildButton]:
*
* @sample androidx.wear.compose.material3.samples.SimpleChildButtonSample
- *
- * Example of a [ChildButton] with onLongClick:
- *
- * @sample androidx.wear.compose.material3.samples.ChildButtonWithOnLongClickSample
* @param onClick Will be called when the user clicks the button
* @param modifier Modifier to be applied to the button
* @param onLongClick Called when this button is long clicked (long-pressed). When this callback is
@@ -423,6 +407,14 @@
* Example of a [Button] with an icon and secondary label:
*
* @sample androidx.wear.compose.material3.samples.ButtonSample
+ *
+ * Example of a [Button] with a large icon and adjusted content padding:
+ *
+ * @sample androidx.wear.compose.material3.samples.ButtonLargeIconSample
+ *
+ * Example of a [Button] with an extra large icon and adjusted content padding:
+ *
+ * @sample androidx.wear.compose.material3.samples.ButtonExtraLargeIconSample
* @param onClick Will be called when the user clicks the button
* @param modifier Modifier to be applied to the button
* @param onLongClick Called when this button is long clicked (long-pressed). When this callback is
@@ -479,9 +471,12 @@
provideNullableScopeContent(
contentColor = colors.secondaryContentColor(enabled),
textStyle = FilledButtonTokens.SecondaryLabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 2,
- textAlign = TextAlign.Start,
+ textConfiguration =
+ TextConfiguration(
+ textAlign = TextAlign.Start,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 2
+ ),
content = secondaryLabel
),
icon = icon,
@@ -496,11 +491,14 @@
provideScopeContent(
contentColor = colors.contentColor(enabled),
textStyle = FilledButtonTokens.LabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 3,
- textAlign =
- if (icon != null || secondaryLabel != null) TextAlign.Start
- else TextAlign.Center,
+ textConfiguration =
+ TextConfiguration(
+ textAlign =
+ if (icon != null || secondaryLabel != null) TextAlign.Start
+ else TextAlign.Center,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 3
+ ),
content = label
)
)
@@ -594,9 +592,12 @@
provideNullableScopeContent(
contentColor = colors.secondaryContentColor(enabled),
textStyle = FilledButtonTokens.SecondaryLabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 2,
- textAlign = TextAlign.Start,
+ textConfiguration =
+ TextConfiguration(
+ textAlign = TextAlign.Start,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 2,
+ ),
content = secondaryLabel
),
icon = icon,
@@ -611,11 +612,14 @@
provideScopeContent(
contentColor = colors.contentColor(enabled),
textStyle = FilledButtonTokens.LabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 3,
- textAlign =
- if (icon != null || secondaryLabel != null) TextAlign.Start
- else TextAlign.Center,
+ textConfiguration =
+ TextConfiguration(
+ textAlign =
+ if (icon != null || secondaryLabel != null) TextAlign.Start
+ else TextAlign.Center,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 3,
+ ),
content = label
)
)
@@ -704,9 +708,12 @@
provideNullableScopeContent(
contentColor = colors.secondaryContentColor(enabled),
textStyle = FilledButtonTokens.SecondaryLabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 2,
- textAlign = TextAlign.Start,
+ textConfiguration =
+ TextConfiguration(
+ textAlign = TextAlign.Start,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 2,
+ ),
content = secondaryLabel
),
icon = icon,
@@ -721,11 +728,14 @@
provideScopeContent(
contentColor = colors.contentColor(enabled),
textStyle = FilledButtonTokens.LabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 3,
- textAlign =
- if (icon != null || secondaryLabel != null) TextAlign.Start
- else TextAlign.Center,
+ textConfiguration =
+ TextConfiguration(
+ textAlign =
+ if (icon != null || secondaryLabel != null) TextAlign.Start
+ else TextAlign.Center,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 3,
+ ),
content = label
)
)
@@ -814,9 +824,12 @@
provideNullableScopeContent(
contentColor = colors.secondaryContentColor(enabled),
textStyle = FilledButtonTokens.SecondaryLabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 2,
- textAlign = TextAlign.Start,
+ textConfiguration =
+ TextConfiguration(
+ textAlign = TextAlign.Start,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 2,
+ ),
content = secondaryLabel
),
icon = icon,
@@ -831,11 +844,14 @@
provideScopeContent(
contentColor = colors.contentColor(enabled),
textStyle = FilledButtonTokens.LabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 3,
- textAlign =
- if (icon != null || secondaryLabel != null) TextAlign.Start
- else TextAlign.Center,
+ textConfiguration =
+ TextConfiguration(
+ textAlign =
+ if (icon != null || secondaryLabel != null) TextAlign.Start
+ else TextAlign.Center,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 3,
+ ),
content = label
)
)
@@ -957,9 +973,12 @@
provideScopeContent(
contentColor = colors.contentColor(enabled),
textStyle = CompactButtonTokens.LabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 1,
- textAlign = if (icon != null) TextAlign.Start else TextAlign.Center,
+ textConfiguration =
+ TextConfiguration(
+ textAlign = if (icon != null) TextAlign.Start else TextAlign.Center,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1,
+ ),
label
)
)
@@ -1216,7 +1235,7 @@
/**
* Creates a [ButtonColors] for a [Button] with an image background, typically with a scrim over
* the image to ensure that the content is visible. Uses a default content color of
- * [ColorScheme.onSurface].
+ * [ColorScheme.onBackground].
*
* @param backgroundImagePainter The [Painter] to use to draw the background of the [Button]
* @param backgroundImageScrimBrush The [Brush] to use to paint a scrim over the background
@@ -1249,7 +1268,10 @@
)
),
contentColor: Color = ImageButtonTokens.ContentColor.value,
- secondaryContentColor: Color = ImageButtonTokens.SecondaryContentColor.value,
+ secondaryContentColor: Color =
+ ImageButtonTokens.SecondaryContentColor.value.copy(
+ alpha = ImageButtonTokens.SecondaryContentOpacity
+ ),
iconColor: Color = ImageButtonTokens.IconColor.value,
disabledContentColor: Color =
ImageButtonTokens.DisabledContentColor.value.toDisabledColor(
@@ -1274,13 +1296,13 @@
)
}
- val disabledContentAlpha = ImageButtonTokens.DisabledContentOpacity
+ val disabledContainerAlpha = ImageButtonTokens.DisabledContainerOpacity
val disabledBackgroundPainter =
- remember(backgroundImagePainter, backgroundImageScrimBrush, disabledContentAlpha) {
+ remember(backgroundImagePainter, backgroundImageScrimBrush, disabledContainerAlpha) {
androidx.wear.compose.materialcore.ImageWithScrimPainter(
imagePainter = backgroundImagePainter,
brush = backgroundImageScrimBrush,
- alpha = disabledContentAlpha,
+ alpha = disabledContainerAlpha,
forcedSize = forcedSize,
)
}
@@ -1363,7 +1385,16 @@
disabledIconColor = disabledIconColor
)
+ /** The recommended horizontal padding used by [Button] by default */
val ButtonHorizontalPadding = 14.dp
+
+ /** The recommended start padding to be used with [Button] with a large icon */
+ val ButtonLargeIconStartPadding = 12.dp
+
+ /** The recommended start padding to be used with [Button] with an extra large icon */
+ val ButtonExtraLargeIconStartPadding = 8.dp
+
+ /** The recommended vertical padding used by [Button] by default */
val ButtonVerticalPadding = 6.dp
/** The default content padding used by [Button] */
@@ -1373,12 +1404,36 @@
vertical = ButtonVerticalPadding,
)
+ /** The default content padding used by [Button] with a large icon */
+ val ButtonWithLargeIconContentPadding: PaddingValues =
+ PaddingValues(
+ start = ButtonLargeIconStartPadding,
+ top = ButtonVerticalPadding,
+ end = ButtonHorizontalPadding,
+ bottom = ButtonVerticalPadding
+ )
+
+ /** The default content padding used by [Button] with an extra large icon */
+ val ButtonWithExtraLargeIconContentPadding: PaddingValues =
+ PaddingValues(
+ start = ButtonExtraLargeIconStartPadding,
+ top = ButtonVerticalPadding,
+ end = ButtonHorizontalPadding,
+ bottom = ButtonVerticalPadding
+ )
+
+ /** The size of the icon when used inside a "[CompactButton]. */
+ val SmallIconSize: Dp = CompactButtonTokens.IconSize
+
/** The default size of the icon when used inside a [Button]. */
val IconSize: Dp = FilledButtonTokens.IconSize
- /** The size of the icon when used inside a Large "Avatar" [Button]. */
+ /** The recommended icon size when used in [Button]s for icons such as an app icon */
val LargeIconSize: Dp = FilledButtonTokens.IconLargeSize
+ /** The recommended icon size when used in [Button]s for icons such as an avatar icon */
+ val ExtraLargeIconSize: Dp = FilledButtonTokens.IconExtraLargeSize
+
/**
* The default height applied for the [Button]. Note that you can override it by applying
* Modifier.heightIn directly on [Button].
@@ -1419,9 +1474,6 @@
/** The height to be applied for a large [EdgeButton]. */
val EdgeButtonHeightLarge = 96.dp
- /** The size of the icon when used inside a "[CompactButton]. */
- val SmallIconSize: Dp = CompactButtonTokens.IconSize
-
/**
* The default padding to be provided around a [CompactButton] in order to ensure that its
* tappable area meets minimum UX guidance.
@@ -1778,8 +1830,12 @@
val borderModifier =
if (border != null) modifier.border(border = border, shape = shape) else modifier
Row(
+ verticalAlignment = Alignment.CenterVertically,
+ // Fill the container height but not its width as buttons have fixed size height but we
+ // want them to be able to fit their content
modifier =
borderModifier
+ .fillMaxHeight()
.clip(shape = shape)
.width(intrinsicSize = IntrinsicSize.Max)
.paint(
@@ -1834,25 +1890,18 @@
contentPadding = contentPadding,
interactionSource = interactionSource,
) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- // Fill the container height but not its width as buttons have fixed size height but we
- // want them to be able to fit their content
- modifier = Modifier.fillMaxHeight()
- ) {
- if (icon != null) {
- Box(
- modifier = Modifier.wrapContentSize(align = Alignment.Center),
- content = provideScopeContent(colors.iconColor(enabled), icon)
- )
- Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
- }
- Column {
- Row(content = labelContent)
- if (secondaryLabelContent != null) {
- Spacer(modifier = Modifier.size(2.dp))
- Row(content = secondaryLabelContent)
- }
+ if (icon != null) {
+ Box(
+ modifier = Modifier.wrapContentSize(align = Alignment.Center),
+ content = provideScopeContent(colors.iconColor(enabled), icon)
+ )
+ Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
+ }
+ Column {
+ Row(content = labelContent)
+ if (secondaryLabelContent != null) {
+ Spacer(modifier = Modifier.size(2.dp))
+ Row(content = secondaryLabelContent)
}
}
}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/CheckboxButton.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/CheckboxButton.kt
index 119ca2e..0d92dfa 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/CheckboxButton.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/CheckboxButton.kt
@@ -130,9 +130,12 @@
provideScopeContent(
contentColor = colors.contentColor(enabled = enabled, checked),
textStyle = CheckboxButtonTokens.LabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 3,
- textAlign = TextAlign.Start,
+ textConfiguration =
+ TextConfiguration(
+ textAlign = TextAlign.Start,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 3,
+ ),
content = label
),
toggleControl = {
@@ -158,9 +161,12 @@
provideNullableScopeContent(
contentColor = colors.secondaryContentColor(enabled = enabled, checked),
textStyle = CheckboxButtonTokens.SecondaryLabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 2,
- textAlign = TextAlign.Start,
+ textConfiguration =
+ TextConfiguration(
+ textAlign = TextAlign.Start,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 2,
+ ),
content = secondaryLabel
),
background = { isEnabled, isChecked ->
@@ -286,9 +292,12 @@
provideScopeContent(
contentColor = colors.contentColor(enabled = enabled, checked = checked),
textStyle = SplitCheckboxButtonTokens.LabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 3,
- textAlign = TextAlign.Start,
+ textConfiguration =
+ TextConfiguration(
+ textAlign = TextAlign.Start,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 3,
+ ),
content = label
),
secondaryLabel =
@@ -296,9 +305,12 @@
contentColor =
colors.secondaryContentColor(enabled = enabled, checked = checked),
textStyle = SplitCheckboxButtonTokens.SecondaryLabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 2,
- textAlign = TextAlign.Start,
+ textConfiguration =
+ TextConfiguration(
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 2,
+ textAlign = TextAlign.Start,
+ ),
content = secondaryLabel
),
)
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Confirmation.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Confirmation.kt
index 0a94d14..502cafaa 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Confirmation.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Confirmation.kt
@@ -45,6 +45,7 @@
import androidx.compose.ui.platform.LocalAccessibilityManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.wear.compose.foundation.CurvedDirection
@@ -185,8 +186,12 @@
CompositionLocalProvider(
LocalContentColor provides colors.textColor,
LocalTextStyle provides MaterialTheme.typography.titleMedium,
- LocalTextAlign provides TextAlign.Center,
- LocalTextMaxLines provides ConfirmationDefaults.LinearContentMaxLines
+ LocalTextConfiguration provides
+ TextConfiguration(
+ textAlign = TextAlign.Center,
+ maxLines = ConfirmationDefaults.LinearContentMaxLines,
+ overflow = TextOverflow.Ellipsis
+ ),
) {
if (text != null) {
Spacer(Modifier.height(ConfirmationDefaults.LinearContentSpacing))
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/EdgeButton.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/EdgeButton.kt
index faccf84..786b647 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/EdgeButton.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/EdgeButton.kt
@@ -258,9 +258,12 @@
provideScopeContent(
colors.contentColor(enabled = enabled),
MaterialTheme.typography.labelMedium,
- TextOverflow.Ellipsis,
- maxLines = 3, // TODO(): Change according to buttonHeight
- TextAlign.Center,
+ textConfiguration =
+ TextConfiguration(
+ TextAlign.Center,
+ TextOverflow.Ellipsis,
+ maxLines = 3, // TODO(): Change according to buttonHeight
+ ),
content
)
)
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Providers.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Providers.kt
index a176a12..d1ca985 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Providers.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Providers.kt
@@ -21,8 +21,6 @@
import androidx.compose.runtime.State
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.text.style.TextOverflow
internal fun <T> provideScopeContent(
contentColor: Color,
@@ -40,17 +38,13 @@
internal fun <T> provideScopeContent(
contentColor: Color,
textStyle: TextStyle,
- overflow: TextOverflow,
- maxLines: Int,
- textAlign: TextAlign,
+ textConfiguration: TextConfiguration,
content: (@Composable T.() -> Unit)
): (@Composable T.() -> Unit) = {
CompositionLocalProvider(
LocalContentColor provides contentColor,
LocalTextStyle provides textStyle,
- LocalTextOverflow provides overflow,
- LocalTextMaxLines provides maxLines,
- LocalTextAlign provides textAlign
+ LocalTextConfiguration provides textConfiguration,
) {
content()
}
@@ -73,18 +67,14 @@
internal fun <T> provideScopeContent(
contentColor: State<Color>,
textStyle: TextStyle,
- overflow: TextOverflow,
- maxLines: Int,
- textAlign: TextAlign,
+ textConfiguration: TextConfiguration,
content: (@Composable T.() -> Unit)
): (@Composable T.() -> Unit) = {
val color = contentColor.value
CompositionLocalProvider(
LocalContentColor provides color,
LocalTextStyle provides textStyle,
- LocalTextOverflow provides overflow,
- LocalTextMaxLines provides maxLines,
- LocalTextAlign provides textAlign
+ LocalTextConfiguration provides textConfiguration,
) {
content()
}
@@ -132,9 +122,7 @@
internal fun <T> provideNullableScopeContent(
contentColor: State<Color>,
textStyle: TextStyle,
- overflow: TextOverflow,
- maxLines: Int,
- textAlign: TextAlign,
+ textConfiguration: TextConfiguration,
content: (@Composable T.() -> Unit)?
): (@Composable T.() -> Unit)? =
content?.let {
@@ -143,9 +131,7 @@
CompositionLocalProvider(
LocalContentColor provides color,
LocalTextStyle provides textStyle,
- LocalTextOverflow provides overflow,
- LocalTextMaxLines provides maxLines,
- LocalTextAlign provides textAlign,
+ LocalTextConfiguration provides textConfiguration,
) {
content()
}
@@ -170,9 +156,7 @@
internal fun <T> provideNullableScopeContent(
contentColor: Color,
textStyle: TextStyle,
- overflow: TextOverflow,
- maxLines: Int,
- textAlign: TextAlign,
+ textConfiguration: TextConfiguration,
content: (@Composable T.() -> Unit)?
): (@Composable T.() -> Unit)? =
content?.let {
@@ -180,9 +164,7 @@
CompositionLocalProvider(
LocalContentColor provides contentColor,
LocalTextStyle provides textStyle,
- LocalTextOverflow provides overflow,
- LocalTextMaxLines provides maxLines,
- LocalTextAlign provides textAlign,
+ LocalTextConfiguration provides textConfiguration,
) {
content()
}
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 858243c..eab5a42 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
@@ -164,9 +164,12 @@
provideScopeContent(
contentColor = colors.contentColor(enabled = enabled, selected = selected),
textStyle = RadioButtonTokens.LabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 3,
- textAlign = TextAlign.Start,
+ textConfiguration =
+ TextConfiguration(
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 3,
+ textAlign = TextAlign.Start,
+ ),
content = label
),
secondaryLabel =
@@ -174,9 +177,12 @@
contentColor =
colors.secondaryContentColor(enabled = enabled, selected = selected),
textStyle = RadioButtonTokens.SecondaryLabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 2,
- textAlign = TextAlign.Start,
+ textConfiguration =
+ TextConfiguration(
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 2,
+ textAlign = TextAlign.Start,
+ ),
content = secondaryLabel
)
)
@@ -304,9 +310,12 @@
provideScopeContent(
contentColor = colors.contentColor(enabled = enabled, selected = selected),
textStyle = SplitRadioButtonTokens.LabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 3,
- textAlign = TextAlign.Start,
+ textConfiguration =
+ TextConfiguration(
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 3,
+ textAlign = TextAlign.Start,
+ ),
content = label
),
secondaryLabel =
@@ -314,9 +323,12 @@
contentColor =
colors.secondaryContentColor(enabled = enabled, selected = selected),
textStyle = SplitRadioButtonTokens.SecondaryLabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 2,
- textAlign = TextAlign.Start,
+ textConfiguration =
+ TextConfiguration(
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 2,
+ textAlign = TextAlign.Start,
+ ),
content = secondaryLabel
),
)
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SegmentedCircularProgressIndicator.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SegmentedCircularProgressIndicator.kt
index 3057a0b..63417be 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SegmentedCircularProgressIndicator.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SegmentedCircularProgressIndicator.kt
@@ -93,6 +93,10 @@
* Example of [SegmentedCircularProgressIndicator] where the segments are turned on/off:
*
* @sample androidx.wear.compose.material3.samples.SegmentedProgressIndicatorOnOffSample
+ *
+ * Example of smaller size [SegmentedCircularProgressIndicator]:
+ *
+ * @sample androidx.wear.compose.material3.samples.SmallSegmentedProgressIndicatorSample
* @param segmentCount Number of equal segments that the progress indicator should be divided into.
* Has to be a number equal or greater to 1.
* @param completed A function that for each segment between 1..[segmentCount] returns true if this
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SwitchButton.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SwitchButton.kt
index 416c536..f736828 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SwitchButton.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SwitchButton.kt
@@ -137,9 +137,12 @@
provideScopeContent(
contentColor = colors.contentColor(enabled = enabled, checked),
textStyle = SwitchButtonTokens.LabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 3,
- textAlign = TextAlign.Start,
+ textConfiguration =
+ TextConfiguration(
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 3,
+ textAlign = TextAlign.Start,
+ ),
content = label
),
toggleControl = {
@@ -171,9 +174,12 @@
provideNullableScopeContent(
contentColor = colors.secondaryContentColor(enabled = enabled, checked),
textStyle = SwitchButtonTokens.SecondaryLabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 2,
- textAlign = TextAlign.Start,
+ textConfiguration =
+ TextConfiguration(
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 2,
+ textAlign = TextAlign.Start,
+ ),
content = secondaryLabel
),
background = { isEnabled, isChecked ->
@@ -300,9 +306,12 @@
provideScopeContent(
contentColor = colors.contentColor(enabled = enabled, checked = checked),
textStyle = SplitSwitchButtonTokens.LabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 3,
- textAlign = TextAlign.Start,
+ textConfiguration =
+ TextConfiguration(
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 3,
+ textAlign = TextAlign.Start,
+ ),
content = label
),
secondaryLabel =
@@ -310,9 +319,12 @@
contentColor =
colors.secondaryContentColor(enabled = enabled, checked = checked),
textStyle = SplitSwitchButtonTokens.SecondaryLabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 2,
- textAlign = TextAlign.Start,
+ textConfiguration =
+ TextConfiguration(
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 2,
+ textAlign = TextAlign.Start,
+ ),
content = secondaryLabel
),
spacerSize = SwitchButtonDefaults.LabelSpacerSize
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Text.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Text.kt
index 54fddbc..f222e17 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Text.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Text.kt
@@ -98,11 +98,11 @@
fontFamily: FontFamily? = null,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
- textAlign: TextAlign? = LocalTextAlign.current,
+ textAlign: TextAlign? = LocalTextConfiguration.current.textAlign,
lineHeight: TextUnit = TextUnit.Unspecified,
- overflow: TextOverflow = LocalTextOverflow.current,
+ overflow: TextOverflow = LocalTextConfiguration.current.overflow,
softWrap: Boolean = true,
- maxLines: Int = LocalTextMaxLines.current,
+ maxLines: Int = LocalTextConfiguration.current.maxLines,
minLines: Int = 1,
onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current
@@ -192,11 +192,11 @@
fontFamily: FontFamily? = null,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
- textAlign: TextAlign? = LocalTextAlign.current,
+ textAlign: TextAlign? = LocalTextConfiguration.current.textAlign,
lineHeight: TextUnit = TextUnit.Unspecified,
- overflow: TextOverflow = LocalTextOverflow.current,
+ overflow: TextOverflow = LocalTextConfiguration.current.overflow,
softWrap: Boolean = true,
- maxLines: Int = LocalTextMaxLines.current,
+ maxLines: Int = LocalTextConfiguration.current.maxLines,
minLines: Int = 1,
inlineContent: Map<String, InlineTextContent> = mapOf(),
onTextLayout: (TextLayoutResult) -> Unit = {},
@@ -250,27 +250,47 @@
}
/**
- * CompositionLocal containing the preferred max lines that will be used by [Text] components by
- * default. Material3 components related to text such as [Button], [CheckboxButton], [SwitchButton],
- * [RadioButton] will use [LocalTextMaxLines] to set values with which to style child text
+ * CompositionLocal containing the preferred [TextConfiguration] that will be used by [Text]
+ * components by default consisting of text alignment, overflow specification and max lines.
+ * Material3 components related to text such as [Button], [CheckboxButton], [SwitchButton],
+ * [RadioButton] use [LocalTextConfiguration] to set values with which to style child text
* components.
*/
-val LocalTextMaxLines: ProvidableCompositionLocal<Int> =
- compositionLocalOf(structuralEqualityPolicy()) { Int.MAX_VALUE }
+val LocalTextConfiguration: ProvidableCompositionLocal<TextConfiguration> =
+ compositionLocalOf(structuralEqualityPolicy()) {
+ TextConfiguration(
+ TextConfigurationDefaults.TextAlign,
+ TextConfigurationDefaults.Overflow,
+ TextConfigurationDefaults.MaxLines
+ )
+ }
-/**
- * CompositionLocal containing the preferred [TextOverflow] that will be used by [Text] components
- * by default. Material3 components related to text such as [Button], [CheckboxButton],
- * [SwitchButton], [RadioButton] will use [LocalTextOverflow] to set values with which to style
- * child text components.
- */
-val LocalTextOverflow: ProvidableCompositionLocal<TextOverflow> =
- compositionLocalOf(structuralEqualityPolicy()) { TextOverflow.Clip }
+class TextConfiguration(
+ val textAlign: TextAlign?,
+ val overflow: TextOverflow,
+ val maxLines: Int,
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || other !is TextConfiguration) return false
-/**
- * CompositionLocal containing the preferred [TextAlign] that will be used by [Text] components by
- * default. Material3 components related to text such as [Button], [CheckboxButton], [SwitchButton],
- * [RadioButton] will use [LocalTextAlign] to set values with which to style child text components.
- */
-val LocalTextAlign: ProvidableCompositionLocal<TextAlign?> =
- compositionLocalOf(structuralEqualityPolicy()) { null }
+ if (textAlign != other.textAlign) return false
+ if (overflow != other.overflow) return false
+ if (maxLines != other.maxLines) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = textAlign.hashCode()
+ result = 31 * result + overflow.hashCode()
+ result = 31 * result + maxLines.hashCode()
+ return result
+ }
+}
+
+object TextConfigurationDefaults {
+ val TextAlign: TextAlign? = null
+ val Overflow: TextOverflow = TextOverflow.Clip
+ const val MaxLines: Int = Int.MAX_VALUE
+}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Typography.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Typography.kt
index a5bc319..f210918 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Typography.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Typography.kt
@@ -56,6 +56,9 @@
* Arc text styles are used for curved text making up the signposting on the UI such as time text
* and curved labels, a tailored font axis that specifically optimizes type along a curve.
*
+ * @property arcLarge ArcLarge is for arc headers and titles. Arc is for text along a curved path on
+ * the screen, reserved for short header text strings at the very top or bottom of the screen like
+ * confirmation overlays.
* @property arcMedium ArcMedium is for arc headers and titles. Arc is for text along a curved path
* on the screen, reserved for short header text strings at the very top or bottom of the screen
* like page titles.
@@ -112,6 +115,7 @@
@Immutable
class Typography
internal constructor(
+ val arcLarge: TextStyle,
val arcMedium: TextStyle,
val arcSmall: TextStyle,
val displayLarge: TextStyle,
@@ -135,6 +139,7 @@
) {
constructor(
defaultFontFamily: FontFamily = FontFamily.Default,
+ arcLarge: TextStyle = TypographyTokens.ArcLarge,
arcMedium: TextStyle = TypographyTokens.ArcMedium,
arcSmall: TextStyle = TypographyTokens.ArcSmall,
displayLarge: TextStyle = TypographyTokens.DisplayLarge,
@@ -156,6 +161,7 @@
numeralSmall: TextStyle = TypographyTokens.NumeralSmall,
numeralExtraSmall: TextStyle = TypographyTokens.NumeralExtraSmall,
) : this(
+ arcLarge = arcLarge.withDefaultFontFamily(defaultFontFamily),
arcMedium = arcMedium.withDefaultFontFamily(defaultFontFamily),
arcSmall = arcSmall.withDefaultFontFamily(defaultFontFamily),
displayLarge = displayLarge.withDefaultFontFamily(defaultFontFamily),
@@ -180,6 +186,7 @@
/** Returns a copy of this Typography, optionally overriding some of the values. */
fun copy(
+ arcLarge: TextStyle = this.arcLarge,
arcMedium: TextStyle = this.arcMedium,
arcSmall: TextStyle = this.arcSmall,
displayLarge: TextStyle = this.displayLarge,
@@ -202,6 +209,7 @@
numeralExtraSmall: TextStyle = this.numeralExtraSmall,
): Typography =
Typography(
+ arcLarge,
arcMedium,
arcSmall,
displayLarge,
@@ -228,6 +236,7 @@
if (this === other) return true
if (other !is Typography) return false
+ if (arcLarge != other.arcLarge) return false
if (arcMedium != other.arcMedium) return false
if (arcSmall != other.arcSmall) return false
if (displayLarge != other.displayLarge) return false
@@ -253,7 +262,8 @@
}
override fun hashCode(): Int {
- var result = arcMedium.hashCode()
+ var result = arcLarge.hashCode()
+ result = 31 * result + arcMedium.hashCode()
result = 31 * result + arcSmall.hashCode()
result = 31 * result + displayLarge.hashCode()
result = 31 * result + displayMedium.hashCode()
@@ -278,6 +288,7 @@
override fun toString(): String {
return "Typography(" +
+ "arcLarge=$arcLarge, " +
"arcMedium=$arcMedium, " +
"arcSmall=$arcSmall, " +
"displayLarge=$displayLarge, " +
@@ -327,6 +338,7 @@
/** Helper function for typography tokens. */
internal fun Typography.fromToken(value: TypographyKeyTokens): TextStyle {
return when (value) {
+ TypographyKeyTokens.ArcLarge -> arcLarge
TypographyKeyTokens.ArcMedium -> arcMedium
TypographyKeyTokens.ArcSmall -> arcSmall
TypographyKeyTokens.DisplayLarge -> displayLarge
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/FilledButtonTokens.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/FilledButtonTokens.kt
index e01a22d..f2501f5 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/FilledButtonTokens.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/FilledButtonTokens.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-// VERSION: v0_68
+// VERSION: v0_73
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.wear.compose.material3.tokens
@@ -30,8 +30,9 @@
val DisabledContentColor = ColorSchemeKeyTokens.OnSurface
val DisabledContentOpacity = 0.38f
val IconColor = ColorSchemeKeyTokens.OnPrimary
+ val IconExtraLargeSize = 36.0.dp
val IconLargeSize = 32.0.dp
- val IconSize = 24.0.dp
+ val IconSize = 26.0.dp
val LabelColor = ColorSchemeKeyTokens.OnPrimary
val LabelFont = TypographyKeyTokens.LabelMedium
val SecondaryLabelColor = ColorSchemeKeyTokens.OnPrimary
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/ImageButtonTokens.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/ImageButtonTokens.kt
index 4af2dad..a95e41f 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/ImageButtonTokens.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/ImageButtonTokens.kt
@@ -14,17 +14,19 @@
* limitations under the License.
*/
-// VERSION: v0_65
+// VERSION: 0_72
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.wear.compose.material3.tokens
internal object ImageButtonTokens {
val BackgroundImageGradientColor = ColorSchemeKeyTokens.SurfaceContainer
- val ContentColor = ColorSchemeKeyTokens.OnSurface
+ val ContentColor = ColorSchemeKeyTokens.OnBackground
+ val DisabledContainerOpacity = 0.12f
val DisabledContentColor = ColorSchemeKeyTokens.OnSurface
val DisabledContentOpacity = 0.38f
val GradientEndOpacity = 0.0f
val GradientStartOpacity = 1.0f
- val IconColor = ColorSchemeKeyTokens.OnSurface
- val SecondaryContentColor = ColorSchemeKeyTokens.OnSurface
+ val IconColor = ColorSchemeKeyTokens.OnBackground
+ val SecondaryContentColor = ColorSchemeKeyTokens.OnBackground
+ val SecondaryContentOpacity = 0.8f
}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypeScaleTokens.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypeScaleTokens.kt
index 0190368..be763e7 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypeScaleTokens.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypeScaleTokens.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-// VERSION: v0_65
+// VERSION: v0_71
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.wear.compose.material3.tokens
@@ -22,19 +22,23 @@
import androidx.compose.ui.unit.sp
internal object TypeScaleTokens {
+ val ArcLargeFont = TypefaceTokens.Brand
+ val ArcLargeLineHeight = 22.0.sp
+ val ArcLargeSize = 20.sp
+ val ArcLargeTracking = 0.4.sp
+ val ArcLargeWeight = 600.0f
+ val ArcLargeWidth = 100.0f
val ArcMediumFont = TypefaceTokens.Brand
val ArcMediumLineHeight = 18.0.sp
val ArcMediumSize = 15.sp
- val ArcMediumTracking = 0.2.sp
+ val ArcMediumTracking = 0.6.sp
val ArcMediumWeight = 600.0f
- val ArcMediumWeightProminent = 800.0f
val ArcMediumWidth = 100.0f
val ArcSmallFont = TypefaceTokens.Brand
val ArcSmallLineHeight = 16.0.sp
val ArcSmallSize = 14.sp
- val ArcSmallTracking = 0.2.sp
+ val ArcSmallTracking = 0.6.sp
val ArcSmallWeight = 560.0f
- val ArcSmallWeightProminent = 760.0f
val ArcSmallWidth = 100.0f
val BodyExtraSmallFont = TypefaceTokens.Brand
val BodyExtraSmallLineHeight = 12.0.sp
@@ -65,7 +69,7 @@
val BodySmallWeightProminent = 700.0f
val BodySmallWidth = 110.0f
val DisplayLargeFont = TypefaceTokens.Brand
- val DisplayLargeLineHeight = 44.0.sp
+ val DisplayLargeLineHeight = 46.0.sp
val DisplayLargeSize = 40.sp
val DisplayLargeTracking = 0.2.sp
val DisplayLargeWeight = 500.0f
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypefaceTokens.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypefaceTokens.kt
index aeb00d3..0779680 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypefaceTokens.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypefaceTokens.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-// VERSION: v0_65
+// VERSION: v0_71
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.wear.compose.material3.tokens
@@ -24,7 +24,7 @@
internal object TypefaceTokens {
val Brand = FontFamily.SansSerif
+ val Plain = FontFamily.SansSerif
val WeightBold = FontWeight.Bold
- val WeightMedium = FontWeight.Medium
val WeightRegular = FontWeight.Normal
}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypographyKeyTokens.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypographyKeyTokens.kt
index bb04daa..ea1dec4 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypographyKeyTokens.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypographyKeyTokens.kt
@@ -14,12 +14,13 @@
* limitations under the License.
*/
-// VERSION: v0_65
+// VERSION: v0_71
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.wear.compose.material3.tokens
internal enum class TypographyKeyTokens {
+ ArcLarge,
ArcMedium,
ArcSmall,
BodyExtraSmall,
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypographyTokens.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypographyTokens.kt
index cfb310e..9165f0d 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypographyTokens.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypographyTokens.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-// VERSION: v0_65
+// VERSION: v0_71
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.wear.compose.material3.tokens
@@ -26,6 +26,20 @@
import androidx.wear.compose.material3.DefaultTextStyle
internal object TypographyTokens {
+ val ArcLarge =
+ DefaultTextStyle.copy(
+ fontFamily =
+ Font(
+ DeviceFontFamilyName(TypeScaleTokens.ArcLargeFont.name),
+ weight = FontWeight(TypeScaleTokens.ArcLargeWeight.toInt()),
+ variationSettings = TypographyVariableFontsTokens.ArcLargeVariationSettings,
+ )
+ .toFontFamily(),
+ fontWeight = FontWeight(TypeScaleTokens.ArcLargeWeight.toInt()),
+ fontSize = TypeScaleTokens.ArcLargeSize,
+ lineHeight = TypeScaleTokens.ArcLargeLineHeight,
+ letterSpacing = TypeScaleTokens.ArcLargeTracking,
+ )
val ArcMedium =
DefaultTextStyle.copy(
fontFamily =
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypographyVariableFontsTokens.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypographyVariableFontsTokens.kt
index dd22c78..6169c9f 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypographyVariableFontsTokens.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypographyVariableFontsTokens.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-// VERSION: v0_65
+// VERSION: v0_71
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.wear.compose.material3.tokens
@@ -22,10 +22,15 @@
import androidx.compose.ui.text.font.FontVariation
internal object TypographyVariableFontsTokens {
+ val ArcLargeVariationSettings =
+ FontVariation.Settings(
+ FontVariation.Setting("wght", TypeScaleTokens.ArcLargeWeight),
+ FontVariation.Setting("wdth", TypeScaleTokens.ArcLargeWidth),
+ )
val ArcMediumVariationSettings =
FontVariation.Settings(
- FontVariation.Setting("wdth", TypeScaleTokens.ArcMediumWidth),
FontVariation.Setting("wght", TypeScaleTokens.ArcMediumWeight),
+ FontVariation.Setting("wdth", TypeScaleTokens.ArcMediumWidth),
)
val ArcSmallVariationSettings =
FontVariation.Settings(
@@ -39,8 +44,8 @@
)
val BodyLargeVariationSettings =
FontVariation.Settings(
- FontVariation.Setting("wght", TypeScaleTokens.BodyLargeWeight),
FontVariation.Setting("wdth", TypeScaleTokens.BodyLargeWidth),
+ FontVariation.Setting("wght", TypeScaleTokens.BodyLargeWeight),
)
val BodyMediumVariationSettings =
FontVariation.Settings(
@@ -54,13 +59,13 @@
)
val DisplayLargeVariationSettings =
FontVariation.Settings(
- FontVariation.Setting("wdth", TypeScaleTokens.DisplayLargeWidth),
FontVariation.Setting("wght", TypeScaleTokens.DisplayLargeWeight),
+ FontVariation.Setting("wdth", TypeScaleTokens.DisplayLargeWidth),
)
val DisplayMediumVariationSettings =
FontVariation.Settings(
- FontVariation.Setting("wght", TypeScaleTokens.DisplayMediumWeight),
FontVariation.Setting("wdth", TypeScaleTokens.DisplayMediumWidth),
+ FontVariation.Setting("wght", TypeScaleTokens.DisplayMediumWeight),
)
val DisplaySmallVariationSettings =
FontVariation.Settings(
@@ -69,13 +74,13 @@
)
val LabelLargeVariationSettings =
FontVariation.Settings(
- FontVariation.Setting("wght", TypeScaleTokens.LabelLargeWeight),
FontVariation.Setting("wdth", TypeScaleTokens.LabelLargeWidth),
+ FontVariation.Setting("wght", TypeScaleTokens.LabelLargeWeight),
)
val LabelMediumVariationSettings =
FontVariation.Settings(
- FontVariation.Setting("wdth", TypeScaleTokens.LabelMediumWidth),
FontVariation.Setting("wght", TypeScaleTokens.LabelMediumWeight),
+ FontVariation.Setting("wdth", TypeScaleTokens.LabelMediumWidth),
)
val LabelSmallVariationSettings =
FontVariation.Settings(
@@ -94,8 +99,8 @@
)
val NumeralLargeVariationSettings =
FontVariation.Settings(
- FontVariation.Setting("wdth", TypeScaleTokens.NumeralLargeWidth),
FontVariation.Setting("wght", TypeScaleTokens.NumeralLargeWeight),
+ FontVariation.Setting("wdth", TypeScaleTokens.NumeralLargeWidth),
)
val NumeralMediumVariationSettings =
FontVariation.Settings(
@@ -104,8 +109,8 @@
)
val NumeralSmallVariationSettings =
FontVariation.Settings(
- FontVariation.Setting("wdth", TypeScaleTokens.NumeralSmallWidth),
FontVariation.Setting("wght", TypeScaleTokens.NumeralSmallWeight),
+ FontVariation.Setting("wdth", TypeScaleTokens.NumeralSmallWidth),
)
val TitleLargeVariationSettings =
FontVariation.Settings(
@@ -114,8 +119,8 @@
)
val TitleMediumVariationSettings =
FontVariation.Settings(
- FontVariation.Setting("wght", TypeScaleTokens.TitleMediumWeight),
FontVariation.Setting("wdth", TypeScaleTokens.TitleMediumWidth),
+ FontVariation.Setting("wght", TypeScaleTokens.TitleMediumWeight),
)
val TitleSmallVariationSettings =
FontVariation.Settings(
diff --git a/wear/compose/compose-material3/src/main/res/values-af/strings.xml b/wear/compose/compose-material3/src/main/res/values-af/strings.xml
index 4951307..6e2fe18 100644
--- a/wear/compose/compose-material3/src/main/res/values-af/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-af/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d sekonde</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Tydperk"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Dag"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Maand"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Jaar"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Bevestig"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Volgende"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-am/strings.xml b/wear/compose/compose-material3/src/main/res/values-am/strings.xml
index da5ab9c..056cd8a 100644
--- a/wear/compose/compose-material3/src/main/res/values-am/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-am/strings.xml
@@ -33,13 +33,15 @@
<item quantity="other">%d ሰከንዶች</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"ክፍለ ጊዜ"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"ቀን"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"ወር"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"ዓመት"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"አረጋግጥ"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"ቀጣይ"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ar/strings.xml b/wear/compose/compose-material3/src/main/res/values-ar/strings.xml
index 450227f..0daf39f 100644
--- a/wear/compose/compose-material3/src/main/res/values-ar/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ar/strings.xml
@@ -45,13 +45,15 @@
<item quantity="one">ثانية واحدة</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"فترة"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"اليوم"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"الشهر"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"السنة"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"تأكيد"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"التالي"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-as/strings.xml b/wear/compose/compose-material3/src/main/res/values-as/strings.xml
index 178eaa4..5350ef1 100644
--- a/wear/compose/compose-material3/src/main/res/values-as/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-as/strings.xml
@@ -33,13 +33,15 @@
<item quantity="other">%d ছেকেণ্ড</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"পিৰিয়ড"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"দিন"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"মাহ"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"বছৰ"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"নিশ্চিত কৰক"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"পৰৱৰ্তী"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-az/strings.xml b/wear/compose/compose-material3/src/main/res/values-az/strings.xml
index de43d4f..1870b54 100644
--- a/wear/compose/compose-material3/src/main/res/values-az/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-az/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d saniyə</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Müddət"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Gün"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Ay"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"İl"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Təsdiq edin"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Növbəti"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-b+sr+Latn/strings.xml b/wear/compose/compose-material3/src/main/res/values-b+sr+Latn/strings.xml
index eca919a..d95ee6a 100644
--- a/wear/compose/compose-material3/src/main/res/values-b+sr+Latn/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-b+sr+Latn/strings.xml
@@ -36,13 +36,15 @@
<item quantity="other">%d sekundi</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Period"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Dan"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mesec"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Godina"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potvrdi"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Dalje"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-be/strings.xml b/wear/compose/compose-material3/src/main/res/values-be/strings.xml
index dc5e832..b7ee712 100644
--- a/wear/compose/compose-material3/src/main/res/values-be/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-be/strings.xml
@@ -39,13 +39,15 @@
<item quantity="other">%d секунды</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Перыяд"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Дзень"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Месяц"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Год"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Пацвердзіць"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Далей"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-bg/strings.xml b/wear/compose/compose-material3/src/main/res/values-bg/strings.xml
index 2866f08..0872213 100644
--- a/wear/compose/compose-material3/src/main/res/values-bg/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-bg/strings.xml
@@ -38,4 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Година"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Потвърждаване"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Напред"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
+ <skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-bn/strings.xml b/wear/compose/compose-material3/src/main/res/values-bn/strings.xml
index 34a09f1..ea8c427 100644
--- a/wear/compose/compose-material3/src/main/res/values-bn/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-bn/strings.xml
@@ -33,13 +33,15 @@
<item quantity="other">%d সেকেন্ড</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"সময়সীমা"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"দিন"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"মাস"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"বছর"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"কনফার্ম করুন"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"পরবর্তী"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-bs/strings.xml b/wear/compose/compose-material3/src/main/res/values-bs/strings.xml
index 9e53fc8..1c805ea 100644
--- a/wear/compose/compose-material3/src/main/res/values-bs/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-bs/strings.xml
@@ -36,13 +36,15 @@
<item quantity="other">%d sekundi</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Period"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Dan"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mjesec"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Godina"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potvrđivanje"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Naprijed"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ca/strings.xml b/wear/compose/compose-material3/src/main/res/values-ca/strings.xml
index 0c19945..366b32c 100644
--- a/wear/compose/compose-material3/src/main/res/values-ca/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ca/strings.xml
@@ -41,4 +41,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Any"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirma"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Següent"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
+ <skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-cs/strings.xml b/wear/compose/compose-material3/src/main/res/values-cs/strings.xml
index 98dc3ea..58f0233 100644
--- a/wear/compose/compose-material3/src/main/res/values-cs/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-cs/strings.xml
@@ -39,13 +39,15 @@
<item quantity="one">%d sekunda</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Období"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Den"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Měsíc"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Rok"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potvrdit"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Další"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-da/strings.xml b/wear/compose/compose-material3/src/main/res/values-da/strings.xml
index d1bd436..6a7f313 100644
--- a/wear/compose/compose-material3/src/main/res/values-da/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-da/strings.xml
@@ -33,13 +33,15 @@
<item quantity="other">%d sekunder</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Format"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Dag"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Måned"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"År"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Bekræft"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Næste"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-de/strings.xml b/wear/compose/compose-material3/src/main/res/values-de/strings.xml
index 9fdd438..7ce0045 100644
--- a/wear/compose/compose-material3/src/main/res/values-de/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-de/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d Sekunde</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Zeitraum"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Tag"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Monat"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Jahr"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Bestätigen"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Weiter"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-el/strings.xml b/wear/compose/compose-material3/src/main/res/values-el/strings.xml
index 20374c9..2c644ff 100644
--- a/wear/compose/compose-material3/src/main/res/values-el/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-el/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d δευτερόλεπτο</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Περίοδος"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Ημέρα"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Μήνας"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Έτος"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Επιβεβαίωση"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Επόμενο"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-en-rAU/strings.xml b/wear/compose/compose-material3/src/main/res/values-en-rAU/strings.xml
index 58982b2..234b43b6 100644
--- a/wear/compose/compose-material3/src/main/res/values-en-rAU/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-en-rAU/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d second</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Period"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Day"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Month"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Year"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirm"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Next"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-en-rCA/strings.xml b/wear/compose/compose-material3/src/main/res/values-en-rCA/strings.xml
index fb5c42c..c3f406c 100644
--- a/wear/compose/compose-material3/src/main/res/values-en-rCA/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-en-rCA/strings.xml
@@ -38,4 +38,7 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Year"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirm"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Next"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Failed"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Success"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Open on phone"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-en-rGB/strings.xml b/wear/compose/compose-material3/src/main/res/values-en-rGB/strings.xml
index 58982b2..234b43b6 100644
--- a/wear/compose/compose-material3/src/main/res/values-en-rGB/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-en-rGB/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d second</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Period"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Day"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Month"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Year"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirm"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Next"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-en-rIN/strings.xml b/wear/compose/compose-material3/src/main/res/values-en-rIN/strings.xml
index 58982b2..234b43b6 100644
--- a/wear/compose/compose-material3/src/main/res/values-en-rIN/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-en-rIN/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d second</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Period"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Day"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Month"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Year"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirm"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Next"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-en-rXC/strings.xml b/wear/compose/compose-material3/src/main/res/values-en-rXC/strings.xml
index 0bff2a4..b6b8b0a 100644
--- a/wear/compose/compose-material3/src/main/res/values-en-rXC/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-en-rXC/strings.xml
@@ -38,4 +38,7 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Year"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirm"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Next"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Failed"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Success"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Open on phone"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-es-rUS/strings.xml b/wear/compose/compose-material3/src/main/res/values-es-rUS/strings.xml
index 5dac2b5..1eab19a 100644
--- a/wear/compose/compose-material3/src/main/res/values-es-rUS/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-es-rUS/strings.xml
@@ -36,13 +36,12 @@
<item quantity="one">%d segundo</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Período"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Día"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mes"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Año"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmar"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Siguiente"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Error"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Listo"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Abrir en el teléfono"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-es/strings.xml b/wear/compose/compose-material3/src/main/res/values-es/strings.xml
index 7ed6eed..a1be8a0 100644
--- a/wear/compose/compose-material3/src/main/res/values-es/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-es/strings.xml
@@ -36,13 +36,15 @@
<item quantity="one">%d segundo</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Periodo"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Día"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mes"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Año"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmar"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Siguiente"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-et/strings.xml b/wear/compose/compose-material3/src/main/res/values-et/strings.xml
index 898d4f1..2d21a50 100644
--- a/wear/compose/compose-material3/src/main/res/values-et/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-et/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d sekund</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Periood"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Päev"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Kuu"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Aasta"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Kinnita"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Järgmine"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-eu/strings.xml b/wear/compose/compose-material3/src/main/res/values-eu/strings.xml
index 202e7ee..fd184ff 100644
--- a/wear/compose/compose-material3/src/main/res/values-eu/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-eu/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d segundo</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Epea"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Eguna"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Hilabetea"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Urtea"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Berretsi"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Hurrengoa"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-fa/strings.xml b/wear/compose/compose-material3/src/main/res/values-fa/strings.xml
index 8f9c137..0de94ca 100644
--- a/wear/compose/compose-material3/src/main/res/values-fa/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-fa/strings.xml
@@ -33,13 +33,15 @@
<item quantity="other">%d ثانیه</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"مدت زمان"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"روز"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"ماه"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"سال"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"تأیید کردن"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"بعدی"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-fi/strings.xml b/wear/compose/compose-material3/src/main/res/values-fi/strings.xml
index 0f0b50b..872ebb1 100644
--- a/wear/compose/compose-material3/src/main/res/values-fi/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-fi/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d sekunti</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Jakso"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Päivä"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Kuukausi"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Vuosi"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Vahvista"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Seuraava"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-fr-rCA/strings.xml b/wear/compose/compose-material3/src/main/res/values-fr-rCA/strings.xml
index cdc3ad5..9e32ae9 100644
--- a/wear/compose/compose-material3/src/main/res/values-fr-rCA/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-fr-rCA/strings.xml
@@ -36,13 +36,15 @@
<item quantity="other">%d secondes</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Période"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Jour"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mois"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Année"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmer"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Suivant"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-fr/strings.xml b/wear/compose/compose-material3/src/main/res/values-fr/strings.xml
index bdb1f1b..f2c7848 100644
--- a/wear/compose/compose-material3/src/main/res/values-fr/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-fr/strings.xml
@@ -36,13 +36,15 @@
<item quantity="other">%d secondes</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Période"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Jour"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mois"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Année"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmer"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Suivant"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-gl/strings.xml b/wear/compose/compose-material3/src/main/res/values-gl/strings.xml
index ad5ca33..8e4464f 100644
--- a/wear/compose/compose-material3/src/main/res/values-gl/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-gl/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d segundo</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Período"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Día"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mes"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Ano"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmar"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Seguinte"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-gu/strings.xml b/wear/compose/compose-material3/src/main/res/values-gu/strings.xml
index b01e9be..dca740c 100644
--- a/wear/compose/compose-material3/src/main/res/values-gu/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-gu/strings.xml
@@ -33,13 +33,15 @@
<item quantity="other">%d સેકન્ડ</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"અવધિ"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"દિવસ"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"મહિનો"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"વર્ષ"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"કન્ફર્મ કરો"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"આગળ"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-hi/strings.xml b/wear/compose/compose-material3/src/main/res/values-hi/strings.xml
index c861b3af..5f34f55 100644
--- a/wear/compose/compose-material3/src/main/res/values-hi/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-hi/strings.xml
@@ -38,4 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"साल"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"पुष्टि करें"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"अगला"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
+ <skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-hr/strings.xml b/wear/compose/compose-material3/src/main/res/values-hr/strings.xml
index af212a5..7055136 100644
--- a/wear/compose/compose-material3/src/main/res/values-hr/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-hr/strings.xml
@@ -36,13 +36,15 @@
<item quantity="other">%d sekundi</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Razdoblje"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Dan"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mjesec"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Godina"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potvrdi"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Dalje"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-hu/strings.xml b/wear/compose/compose-material3/src/main/res/values-hu/strings.xml
index 2cce16e..3055d32 100644
--- a/wear/compose/compose-material3/src/main/res/values-hu/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-hu/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d másodperc</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Időszak"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Nap"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Hónap"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Év"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Megerősítés"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Következő"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-hy/strings.xml b/wear/compose/compose-material3/src/main/res/values-hy/strings.xml
index cda99db..86d0f53 100644
--- a/wear/compose/compose-material3/src/main/res/values-hy/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-hy/strings.xml
@@ -33,13 +33,15 @@
<item quantity="other">%d վայրկյան</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Ժամանակահատված"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Օր"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Ամիս"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Տարի"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Հաստատել"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Հաջորդը"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-in/strings.xml b/wear/compose/compose-material3/src/main/res/values-in/strings.xml
index 66a973e..07428ce 100644
--- a/wear/compose/compose-material3/src/main/res/values-in/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-in/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d Detik</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Jangka waktu"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Hari"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Bulan"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Tahun"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Konfirmasi"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Berikutnya"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-is/strings.xml b/wear/compose/compose-material3/src/main/res/values-is/strings.xml
index 8983c3f..79fd175 100644
--- a/wear/compose/compose-material3/src/main/res/values-is/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-is/strings.xml
@@ -33,13 +33,15 @@
<item quantity="other">%d sekúndur</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Punktur"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Dagur"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mánuður"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Ár"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Staðfesta"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Áfram"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-it/strings.xml b/wear/compose/compose-material3/src/main/res/values-it/strings.xml
index a9566b7..f592436 100644
--- a/wear/compose/compose-material3/src/main/res/values-it/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-it/strings.xml
@@ -36,13 +36,12 @@
<item quantity="one">%d secondo</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Periodo"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Giorno"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mese"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Anno"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Conferma"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Avanti"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Non riuscita"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Riuscita"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Su smartph."</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-iw/strings.xml b/wear/compose/compose-material3/src/main/res/values-iw/strings.xml
index 91f1e27..5c96861 100644
--- a/wear/compose/compose-material3/src/main/res/values-iw/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-iw/strings.xml
@@ -36,13 +36,15 @@
<item quantity="other">%d שניות</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"תקופת זמן"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"יום"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"חודש"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"שנה"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"אישור"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"הבא"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ja/strings.xml b/wear/compose/compose-material3/src/main/res/values-ja/strings.xml
index 65e9cc7..2a7cff2 100644
--- a/wear/compose/compose-material3/src/main/res/values-ja/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ja/strings.xml
@@ -38,4 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"年"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"確認"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"次へ"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
+ <skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ka/strings.xml b/wear/compose/compose-material3/src/main/res/values-ka/strings.xml
index 4f4734e..f0ff78c 100644
--- a/wear/compose/compose-material3/src/main/res/values-ka/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ka/strings.xml
@@ -38,4 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"წელი"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"დადასტურება"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"შემდეგი"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
+ <skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-kk/strings.xml b/wear/compose/compose-material3/src/main/res/values-kk/strings.xml
index e0d8b3d..c5adc96 100644
--- a/wear/compose/compose-material3/src/main/res/values-kk/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-kk/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d секунд</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Кезең"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Күн"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Ай"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Жыл"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Растау"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Келесі"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-km/strings.xml b/wear/compose/compose-material3/src/main/res/values-km/strings.xml
index 410a6f2..63c6552 100644
--- a/wear/compose/compose-material3/src/main/res/values-km/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-km/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d វិនាទី</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"រយៈពេល"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"ថ្ងៃ"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"ខែ"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"ឆ្នាំ"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"បញ្ជាក់"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"បន្ទាប់"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-kn/strings.xml b/wear/compose/compose-material3/src/main/res/values-kn/strings.xml
index 6ae6994..d35b1ff 100644
--- a/wear/compose/compose-material3/src/main/res/values-kn/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-kn/strings.xml
@@ -33,13 +33,15 @@
<item quantity="other">%d ಸೆಕೆಂಡ್ಗಳು</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"ಅವಧಿ"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"ದಿನ"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"ತಿಂಗಳು"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"ವರ್ಷ"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"ದೃಢೀಕರಿಸಿ"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"ಮುಂದಿನದು"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ko/strings.xml b/wear/compose/compose-material3/src/main/res/values-ko/strings.xml
index 45f8446..6d74f3d 100644
--- a/wear/compose/compose-material3/src/main/res/values-ko/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ko/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d초</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"기간"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"일"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"월"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"년"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"확인"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"다음"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ky/strings.xml b/wear/compose/compose-material3/src/main/res/values-ky/strings.xml
index 692726e..cfbd66f 100644
--- a/wear/compose/compose-material3/src/main/res/values-ky/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ky/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d секунд</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Чекит"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Күн"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Ай"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Жыл"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Ырастоо"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Кийинки"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-lo/strings.xml b/wear/compose/compose-material3/src/main/res/values-lo/strings.xml
index 59d2d94..ad309b0 100644
--- a/wear/compose/compose-material3/src/main/res/values-lo/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-lo/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d ວິນາທີ</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"ໄລຍະເວລາ"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"ມື້"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"ເດືອນ"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"ປີ"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"ຢືນຢັນ"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"ຕໍ່ໄປ"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-lt/strings.xml b/wear/compose/compose-material3/src/main/res/values-lt/strings.xml
index 615054e..135bf3c 100644
--- a/wear/compose/compose-material3/src/main/res/values-lt/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-lt/strings.xml
@@ -39,13 +39,15 @@
<item quantity="other">%d sekundžių</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Laikotarpis"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Diena"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mėnuo"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Metai"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Patvirtinti"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Kitas"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-lv/strings.xml b/wear/compose/compose-material3/src/main/res/values-lv/strings.xml
index 08f2d2c..8832d97 100644
--- a/wear/compose/compose-material3/src/main/res/values-lv/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-lv/strings.xml
@@ -36,13 +36,15 @@
<item quantity="other">%d sekundes</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Periods"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Diena"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mēnesis"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Gads"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Apstiprināt"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Tālāk"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-mk/strings.xml b/wear/compose/compose-material3/src/main/res/values-mk/strings.xml
index a093425..7095f95 100644
--- a/wear/compose/compose-material3/src/main/res/values-mk/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-mk/strings.xml
@@ -33,13 +33,15 @@
<item quantity="other">%d секунди</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Период"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Ден"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Месец"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Година"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Потврди"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Следно"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ml/strings.xml b/wear/compose/compose-material3/src/main/res/values-ml/strings.xml
index 36d8b54..b31e89c 100644
--- a/wear/compose/compose-material3/src/main/res/values-ml/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ml/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d സെക്കൻഡ്</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"കാലയളവ്"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"ദിവസം"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"മാസം"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"വർഷം"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"സ്ഥിരീകരിക്കുക"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"അടുത്തത്"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-mn/strings.xml b/wear/compose/compose-material3/src/main/res/values-mn/strings.xml
index 629e16a..9a14fc2 100644
--- a/wear/compose/compose-material3/src/main/res/values-mn/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-mn/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d секунд</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Хугацаа"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Өдөр"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Сар"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Он"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Баталгаажуулах"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Дараах"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-mr/strings.xml b/wear/compose/compose-material3/src/main/res/values-mr/strings.xml
index 904ad4f..e401899 100644
--- a/wear/compose/compose-material3/src/main/res/values-mr/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-mr/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d सेकंद</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"कालावधी"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"दिवस"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"महिना"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"वर्ष"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"कन्फर्म करा"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"पुढील"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ms/strings.xml b/wear/compose/compose-material3/src/main/res/values-ms/strings.xml
index df4b8c9..c8b0bac 100644
--- a/wear/compose/compose-material3/src/main/res/values-ms/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ms/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d Saat</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Tempoh"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Hari"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Bulan"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Tahun"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Sahkan"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Seterusnya"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-my/strings.xml b/wear/compose/compose-material3/src/main/res/values-my/strings.xml
index aa85494..b7177ea 100644
--- a/wear/compose/compose-material3/src/main/res/values-my/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-my/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d စက္ကန့်</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"အချိန်ကာလ"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"ရက်"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"လ"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"နှစ်"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"အတည်ပြုရန်"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"ရှေ့သို့"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-nb/strings.xml b/wear/compose/compose-material3/src/main/res/values-nb/strings.xml
index 0d9dbd3..5b7bc30 100644
--- a/wear/compose/compose-material3/src/main/res/values-nb/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-nb/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d sekund</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Periode"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Dag"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Måned"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"År"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Bekreft"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Neste"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ne/strings.xml b/wear/compose/compose-material3/src/main/res/values-ne/strings.xml
index 4f5d479..90a3e39 100644
--- a/wear/compose/compose-material3/src/main/res/values-ne/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ne/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d सेकेन्ड</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"अवधि"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"दिन"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"महिना"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"साल"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"पुष्टि गर्नुहोस्"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"अर्को"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-nl/strings.xml b/wear/compose/compose-material3/src/main/res/values-nl/strings.xml
index 8561545..ba319a2 100644
--- a/wear/compose/compose-material3/src/main/res/values-nl/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-nl/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d seconde</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Periode"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Dag"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Maand"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Jaar"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Bevestigen"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Volgende"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-or/strings.xml b/wear/compose/compose-material3/src/main/res/values-or/strings.xml
index 7701b598..eff9baf 100644
--- a/wear/compose/compose-material3/src/main/res/values-or/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-or/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d ସେକେଣ୍ଡ</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"ଅବଧି"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"ଦିନ"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"ମାସ"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"ବର୍ଷ"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"ସୁନିଶ୍ଚିତ କରନ୍ତୁ"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"ପରବର୍ତ୍ତୀ"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-pa/strings.xml b/wear/compose/compose-material3/src/main/res/values-pa/strings.xml
index 1000cde..2b1e623 100644
--- a/wear/compose/compose-material3/src/main/res/values-pa/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-pa/strings.xml
@@ -33,13 +33,15 @@
<item quantity="other">%d ਸਕਿੰਟ</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"ਮਿਆਦ"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"ਦਿਨ"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"ਮਹੀਨਾ"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"ਸਾਲ"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"ਤਸਦੀਕ ਕਰੋ"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"ਅੱਗੇ"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-pl/strings.xml b/wear/compose/compose-material3/src/main/res/values-pl/strings.xml
index 5d56d82..f1805de6 100644
--- a/wear/compose/compose-material3/src/main/res/values-pl/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-pl/strings.xml
@@ -39,13 +39,15 @@
<item quantity="one">%d sekunda</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Kropka"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Dzień"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Miesiąc"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Rok"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potwierdź"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Dalej"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-pt-rBR/strings.xml b/wear/compose/compose-material3/src/main/res/values-pt-rBR/strings.xml
index 77c3710..f3793a0 100644
--- a/wear/compose/compose-material3/src/main/res/values-pt-rBR/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-pt-rBR/strings.xml
@@ -36,13 +36,15 @@
<item quantity="other">%d segundos</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Período"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Dia"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mês"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Ano"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmar"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Avançar"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-pt-rPT/strings.xml b/wear/compose/compose-material3/src/main/res/values-pt-rPT/strings.xml
index 416cbc0..b200da8 100644
--- a/wear/compose/compose-material3/src/main/res/values-pt-rPT/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-pt-rPT/strings.xml
@@ -41,4 +41,7 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Ano"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmar"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Seguinte"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Falhou"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Concluído"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Abrir no tel."</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-pt/strings.xml b/wear/compose/compose-material3/src/main/res/values-pt/strings.xml
index 77c3710..f3793a0 100644
--- a/wear/compose/compose-material3/src/main/res/values-pt/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-pt/strings.xml
@@ -36,13 +36,15 @@
<item quantity="other">%d segundos</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Período"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Dia"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mês"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Ano"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmar"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Avançar"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ro/strings.xml b/wear/compose/compose-material3/src/main/res/values-ro/strings.xml
index f9723df..e1f6fb2 100644
--- a/wear/compose/compose-material3/src/main/res/values-ro/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ro/strings.xml
@@ -36,13 +36,15 @@
<item quantity="one">%d secundă</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Perioada"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Zi"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Lună"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"An"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmă"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Înainte"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ru/strings.xml b/wear/compose/compose-material3/src/main/res/values-ru/strings.xml
index e01e3c3..ba058e1 100644
--- a/wear/compose/compose-material3/src/main/res/values-ru/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ru/strings.xml
@@ -39,13 +39,15 @@
<item quantity="other">%d секунды</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Период"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"День"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Месяц"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Год"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Подтвердить"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Далее"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-si/strings.xml b/wear/compose/compose-material3/src/main/res/values-si/strings.xml
index 6f3d4c6..83a4135 100644
--- a/wear/compose/compose-material3/src/main/res/values-si/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-si/strings.xml
@@ -33,13 +33,15 @@
<item quantity="other">තත්පර %d</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"කාල පරිච්ඡේදය"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"දවස"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"මාසය"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"වසර"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"තහවුරු කරන්න"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"මීළඟ"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-sk/strings.xml b/wear/compose/compose-material3/src/main/res/values-sk/strings.xml
index 8840530..9161ea7 100644
--- a/wear/compose/compose-material3/src/main/res/values-sk/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-sk/strings.xml
@@ -39,13 +39,15 @@
<item quantity="one">%d sekunda</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Obdobie"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Deň"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mesiac"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Rok"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potvrdiť"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Ďalej"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-sl/strings.xml b/wear/compose/compose-material3/src/main/res/values-sl/strings.xml
index cc79d76..f747dfc 100644
--- a/wear/compose/compose-material3/src/main/res/values-sl/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-sl/strings.xml
@@ -44,4 +44,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Leto"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potrdi"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Naprej"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
+ <skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-sq/strings.xml b/wear/compose/compose-material3/src/main/res/values-sq/strings.xml
index 411d53e..e7fe249 100644
--- a/wear/compose/compose-material3/src/main/res/values-sq/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-sq/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d sekondë</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Periudha"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Dita"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Muaji"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Viti"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Konfirmo"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Para"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-sr/strings.xml b/wear/compose/compose-material3/src/main/res/values-sr/strings.xml
index 661b6d2..f33bcf8 100644
--- a/wear/compose/compose-material3/src/main/res/values-sr/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-sr/strings.xml
@@ -36,13 +36,15 @@
<item quantity="other">%d секунди</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Период"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Дан"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Месец"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Година"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Потврди"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Даље"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-sv/strings.xml b/wear/compose/compose-material3/src/main/res/values-sv/strings.xml
index b395ba1d..3aa02d4 100644
--- a/wear/compose/compose-material3/src/main/res/values-sv/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-sv/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d sekund</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Punkt"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Dag"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Månad"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"År"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Bekräfta"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Nästa"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-sw/strings.xml b/wear/compose/compose-material3/src/main/res/values-sw/strings.xml
index aad1959..3d31800 100644
--- a/wear/compose/compose-material3/src/main/res/values-sw/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-sw/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">Sekunde %d</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Kipindi"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Siku"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mwezi"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Mwaka"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Thibitisha"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Endelea"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ta/strings.xml b/wear/compose/compose-material3/src/main/res/values-ta/strings.xml
index aacd3cd..dbb6bf5 100644
--- a/wear/compose/compose-material3/src/main/res/values-ta/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ta/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d வினாடி</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"கால இடைவெளி"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"நாள்"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"மாதம்"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"ஆண்டு"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"உறுதிசெய்யும்"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"அடுத்ததற்குச் செல்லும்"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-te/strings.xml b/wear/compose/compose-material3/src/main/res/values-te/strings.xml
index 2374063..1fc8dea 100644
--- a/wear/compose/compose-material3/src/main/res/values-te/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-te/strings.xml
@@ -38,4 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"సంవత్సరం"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"నిర్ధారించండి"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"తర్వాత"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
+ <skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-th/strings.xml b/wear/compose/compose-material3/src/main/res/values-th/strings.xml
index d1ad793..706dc03 100644
--- a/wear/compose/compose-material3/src/main/res/values-th/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-th/strings.xml
@@ -38,4 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"ปี"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"ยืนยัน"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"ถัดไป"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
+ <skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-tl/strings.xml b/wear/compose/compose-material3/src/main/res/values-tl/strings.xml
index fe20f09..74f7abe 100644
--- a/wear/compose/compose-material3/src/main/res/values-tl/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-tl/strings.xml
@@ -38,4 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Taon"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Kumpirmahin"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Susunod"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
+ <skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-tr/strings.xml b/wear/compose/compose-material3/src/main/res/values-tr/strings.xml
index 3ec35be..4452c7a 100644
--- a/wear/compose/compose-material3/src/main/res/values-tr/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-tr/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d Saniye</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Aralık"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Gün"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Ay"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Yıl"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Onayla"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Sonraki"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-uk/strings.xml b/wear/compose/compose-material3/src/main/res/values-uk/strings.xml
index bb037ea..4faefb1 100644
--- a/wear/compose/compose-material3/src/main/res/values-uk/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-uk/strings.xml
@@ -39,13 +39,15 @@
<item quantity="other">%d секунди</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Період"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"День"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Місяць"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Рік"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Підтвердити"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Далі"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ur/strings.xml b/wear/compose/compose-material3/src/main/res/values-ur/strings.xml
index 6f902ab..34af013 100644
--- a/wear/compose/compose-material3/src/main/res/values-ur/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ur/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d سیکنڈ</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"وقفہ"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"دن"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"مہینہ"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"سال"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"تصدیق کریں"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"اگلا"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-uz/strings.xml b/wear/compose/compose-material3/src/main/res/values-uz/strings.xml
index 4b1121e..c025ed8 100644
--- a/wear/compose/compose-material3/src/main/res/values-uz/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-uz/strings.xml
@@ -38,4 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Yil"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Tasdiqlash"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Keyingisi"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
+ <skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-vi/strings.xml b/wear/compose/compose-material3/src/main/res/values-vi/strings.xml
index 3b40f28..108a554 100644
--- a/wear/compose/compose-material3/src/main/res/values-vi/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-vi/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d giây</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Khoảng thời gian"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Ngày"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Tháng"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Năm"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Xác nhận"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Tiếp theo"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-zh-rCN/strings.xml b/wear/compose/compose-material3/src/main/res/values-zh-rCN/strings.xml
index 14ce25a..78dc434 100644
--- a/wear/compose/compose-material3/src/main/res/values-zh-rCN/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-zh-rCN/strings.xml
@@ -38,4 +38,10 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"年"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"确认"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"下一个"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
+ <skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-zh-rHK/strings.xml b/wear/compose/compose-material3/src/main/res/values-zh-rHK/strings.xml
index 0d30e02..ae0089b 100644
--- a/wear/compose/compose-material3/src/main/res/values-zh-rHK/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-zh-rHK/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d 秒</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"時段"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"日"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"月"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"年"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"確認"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"下一步"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-zh-rTW/strings.xml b/wear/compose/compose-material3/src/main/res/values-zh-rTW/strings.xml
index 3e569a9..59c4018 100644
--- a/wear/compose/compose-material3/src/main/res/values-zh-rTW/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-zh-rTW/strings.xml
@@ -33,13 +33,15 @@
<item quantity="one">%d 秒</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"期間"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"日"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"月"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"年"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"確認"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"下一個"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-zu/strings.xml b/wear/compose/compose-material3/src/main/res/values-zu/strings.xml
index 43167ed..66c1d7a 100644
--- a/wear/compose/compose-material3/src/main/res/values-zu/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-zu/strings.xml
@@ -33,13 +33,15 @@
<item quantity="other">Imizuzwana engu-%d</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Isikhathi"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Usuku"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Inyanga"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Unyaka"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Qinisekisa"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Okulandelayo"</string>
+ <!-- no translation found for wear_m3c_confirmation_failure_message (6690105830380889949) -->
+ <skip />
+ <!-- no translation found for wear_m3c_confirmation_success_message (7045594078216655038) -->
+ <skip />
+ <!-- no translation found for wear_m3c_open_on_phone (5829463187924353601) -->
<skip />
</resources>
diff --git a/wear/compose/compose-navigation/build.gradle b/wear/compose/compose-navigation/build.gradle
index bb35fa2..1e61bdbc 100644
--- a/wear/compose/compose-navigation/build.gradle
+++ b/wear/compose/compose-navigation/build.gradle
@@ -45,7 +45,6 @@
androidTestImplementation(project(":compose:test-utils"))
androidTestImplementation(project(":compose:ui:ui-test-junit4"))
- androidTestImplementation(project(":navigation:navigation-common"))
androidTestImplementation(libs.testRunner)
androidTestImplementation(project(":wear:compose:compose-material"))
androidTestImplementation(project(":wear:compose:compose-navigation-samples"))
diff --git a/wear/protolayout/protolayout-expression/api/current.txt b/wear/protolayout/protolayout-expression/api/current.txt
index b3b2b3e..c3306bc 100644
--- a/wear/protolayout/protolayout-expression/api/current.txt
+++ b/wear/protolayout/protolayout-expression/api/current.txt
@@ -408,6 +408,16 @@
package androidx.wear.protolayout.expression.util {
+ @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=300) public final class DynamicDateFormat {
+ ctor public DynamicDateFormat(String pattern);
+ ctor public DynamicDateFormat(String pattern, optional java.time.ZoneId timeZone);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=300) public androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format();
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=300) public androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format(optional androidx.wear.protolayout.expression.DynamicBuilders.DynamicInstant instant);
+ method public java.time.ZoneId getTimeZone();
+ method public void setTimeZone(java.time.ZoneId);
+ property public final java.time.ZoneId timeZone;
+ }
+
public final class DynamicFormatter {
ctor public DynamicFormatter();
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format(String format, java.lang.Object?... args);
diff --git a/wear/protolayout/protolayout-expression/api/restricted_current.txt b/wear/protolayout/protolayout-expression/api/restricted_current.txt
index b3b2b3e..c3306bc 100644
--- a/wear/protolayout/protolayout-expression/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-expression/api/restricted_current.txt
@@ -408,6 +408,16 @@
package androidx.wear.protolayout.expression.util {
+ @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=300) public final class DynamicDateFormat {
+ ctor public DynamicDateFormat(String pattern);
+ ctor public DynamicDateFormat(String pattern, optional java.time.ZoneId timeZone);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=300) public androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format();
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=300) public androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format(optional androidx.wear.protolayout.expression.DynamicBuilders.DynamicInstant instant);
+ method public java.time.ZoneId getTimeZone();
+ method public void setTimeZone(java.time.ZoneId);
+ property public final java.time.ZoneId timeZone;
+ }
+
public final class DynamicFormatter {
ctor public DynamicFormatter();
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format(String format, java.lang.Object?... args);
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/util/DynamicDateFormat.kt b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/util/DynamicDateFormat.kt
new file mode 100644
index 0000000..8a6e447
--- /dev/null
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/util/DynamicDateFormat.kt
@@ -0,0 +1,296 @@
+/*
+ * 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.expression.util
+
+import android.icu.text.SimpleDateFormat
+import android.icu.util.TimeZone
+import androidx.annotation.VisibleForTesting
+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.RequiresSchemaVersion
+import java.time.Duration
+import java.time.Instant
+import java.time.ZoneId
+import java.util.ArrayDeque
+import java.util.Date
+import java.util.Locale
+
+/**
+ * Equivalent to [android.icu.text.SimpleDateFormat], but generates a [DynamicString] based on a
+ * [DynamicInstant].
+ *
+ * See [android.icu.text.SimpleDateFormat] documentation for the pattern syntax.
+ *
+ * Literal patterns are fully supported, including quotes (`'`) or non-letters (e.g. `:`).
+ *
+ * Currently this implementation only supports hour (`HKhk`), minute (`m`), and AM/PM (`a`)
+ * patterns. Every other letter will throw [IllegalArgumentException].
+ *
+ * NOTE: [DynamicDateFormat] uses `Locale.getDefault(Locale.Category.FORMAT)` at the time of calling
+ * [format] for AM/PM markers. This can change on the remote side, which would cause a mismatch
+ * between the locally-formatted AM/PM and the remotely-formatted numbers, unless the provider sends
+ * a newly formatted [DynamicString] (using a new invocation of [format]).
+ *
+ * Example usage:
+ * ```
+ * // This statement:
+ * DynamicDateFormat(pattern = "HH:mm", timeZone = zone).format(dynamicInstant)
+ * // Generates an equivalent of:
+ * dynamicInstant
+ * .getHour(zone)
+ * .format(DynamicInt32.IntFormatter.Builder().setMinIntegerDigits(2).build())
+ * .concat(DynamicString.constant(":"))
+ * .concat(
+ * dynamicInstant.getMinute(zone)
+ * .format(DynamicInt32.IntFormatter.Builder().setMinIntegerDigits(2).build())
+ * )
+ * ```
+ */
+@RequiresSchemaVersion(major = 1, minor = 300)
+public class DynamicDateFormat
+@VisibleForTesting
+internal constructor(
+ private val pattern: String,
+ // TODO: b/297323092 - Allow providing in the constructor for both local and remote evaluation.
+ // Currently only used for AM/PM.
+ private val locale: Locale?,
+ public var timeZone: ZoneId,
+) {
+
+ @JvmOverloads
+ public constructor(
+ pattern: String,
+ timeZone: ZoneId = ZoneId.systemDefault()
+ ) : this(pattern, locale = null, timeZone)
+
+ init {
+ require(pattern.count { it == '\'' } % 2 == 0) { "Unterminated quote" }
+ }
+
+ private val _locale: Locale
+ get() = locale ?: Locale.getDefault(Locale.Category.FORMAT)
+
+ private val patternParts: List<Part> = extractPatternParts().mergeConstants().toList()
+
+ /**
+ * Formats the [DynamicInstant] (defaults to [DynamicInstant.platformTimeWithSecondsPrecision])
+ * into a date/time [DynamicString].
+ */
+ @RequiresSchemaVersion(major = 1, minor = 300)
+ @JvmOverloads
+ public fun format(
+ instant: DynamicInstant = DynamicInstant.platformTimeWithSecondsPrecision(),
+ ): DynamicString =
+ patternParts
+ .map { it.format(instant) }
+ .ifEmpty { listOf(DynamicString.constant("")) }
+ .reduce { acc, formattedSection -> acc.concat(formattedSection) }
+
+ /** Builds a [Part] sequence from the [pattern]. */
+ private fun extractPatternParts(): Sequence<Part> = sequence {
+ val patternLeft: ArrayDeque<Token> = pattern.tokenize()
+ // Taking tokens from the left and yielding Parts until there's no more tokens.
+ while (patternLeft.isNotEmpty()) {
+ if (patternLeft.first().isUnescapedQuote) {
+ yield(ConstantPart(takeQuotedConstant(patternLeft)))
+ } else if (patternLeft.first().isConstant) {
+ yield(ConstantPart(takeNonLetterConstant(patternLeft)))
+ } else {
+ // Not constant (dynamic pattern).
+ yield(DynamicPart(takeDynamic(patternLeft)))
+ }
+ }
+ }
+
+ /**
+ * Returns everything until the closing quote, and removes it (including the closing quote) from
+ * [patternLeft].
+ *
+ * Assumes the constructor checks that the amount of quotes are even, and all are closed.
+ */
+ private fun takeQuotedConstant(patternLeft: ArrayDeque<Token>): String {
+ patternLeft.removeFirst()
+ val result = patternLeft.takeWhile { !it.isUnescapedQuote }.asString()
+ patternLeft.removeFirst(result.length + 1)
+ return result
+ }
+
+ /** Returns all upcoming non-letter constants, and removes it from [patternLeft]. */
+ private fun takeNonLetterConstant(patternLeft: ArrayDeque<Token>): String {
+ val result = patternLeft.takeWhile { it.isConstant }.asString()
+ patternLeft.removeFirst(result.length)
+ return result
+ }
+
+ /**
+ * Returns the next dynamic section in the pattern, which is basically the repetition of the
+ * first character.
+ */
+ private fun takeDynamic(patternLeft: ArrayDeque<Token>): String =
+ patternLeft
+ // Taking repetitions to determine padding length.
+ .takeWhile { it == patternLeft.first() }
+ .asString()
+ .also { patternLeft.removeFirst(it.length) }
+
+ /** Merges repeated constants to reduce the amount of concatenation nodes. */
+ private fun Sequence<Part>.mergeConstants(): Sequence<Part> = sequence {
+ val empty = ConstantPart("") // Saving an allocation every time we reset lastConstant.
+ var lastConstant = empty
+ forEach { nextSection ->
+ if (nextSection is ConstantPart) {
+ lastConstant += nextSection
+ } else {
+ lastConstant.ifNotEmpty { yield(it) }
+ lastConstant = empty
+ yield(nextSection)
+ }
+ }
+ lastConstant.ifNotEmpty { yield(it) }
+ }
+
+ /** Either a [ConstantPart] or [DynamicPart] part of the pattern. */
+ private sealed interface Part {
+ fun format(instant: DynamicInstant): DynamicString
+ }
+
+ /** A pattern section that is built with [DynamicString.constant]. */
+ private data class ConstantPart(val value: String) : Part {
+ @RequiresSchemaVersion(major = 1, minor = 200)
+ override fun format(instant: DynamicInstant): DynamicString = DynamicString.constant(value)
+
+ operator fun plus(other: ConstantPart) = ConstantPart(value + other.value)
+
+ /** Invokes [block] with `this` if `value != ""`. */
+ inline fun ifNotEmpty(block: (ConstantPart) -> Unit) {
+ if (value.isNotEmpty()) block(this)
+ }
+ }
+
+ /** A pattern section that is formatted into a [DynamicString] based on [~]. */
+ private inner class DynamicPart(code: Char, val length: Int) : Part {
+ private val dynamicBuilder: (DynamicInstant) -> DynamicString
+
+ init {
+ dynamicBuilder =
+ when (code) {
+ 'H' -> this::buildHourInDay0To23
+ 'k' -> this::buildHourInDay1To24
+ 'K' -> this::buildHourInAmPm0To11
+ 'h' -> this::buildHourInAmPm1To12
+ 'm' -> this::buildMinuteInHour
+ 'a' -> this::buildAmPmMarker
+ else -> throw IllegalArgumentException("Illegal pattern character '$code'")
+ }
+ }
+
+ constructor(value: String) : this(value[0], value.length)
+
+ override fun format(instant: DynamicInstant): DynamicString = dynamicBuilder(instant)
+
+ @RequiresSchemaVersion(major = 1, minor = 300)
+ private fun buildHourInDay0To23(instant: DynamicInstant): DynamicString =
+ instant.getHour(timeZone).format(intFormatter)
+
+ @RequiresSchemaVersion(major = 1, minor = 300)
+ private fun buildHourInDay1To24(instant: DynamicInstant): DynamicString {
+ val hour = instant.getHour(timeZone)
+ return DynamicInt32.onCondition(hour.eq(0)).use(24).elseUse(hour).format(intFormatter)
+ }
+
+ @RequiresSchemaVersion(major = 1, minor = 300)
+ private fun buildHourInAmPm0To11(instant: DynamicInstant): DynamicString =
+ instant.getHour(timeZone).rem(12).format(intFormatter)
+
+ @RequiresSchemaVersion(major = 1, minor = 300)
+ private fun buildHourInAmPm1To12(instant: DynamicInstant): DynamicString {
+ val hourRem12: DynamicInt32 = instant.getHour(timeZone).rem(12)
+ return DynamicInt32.onCondition(hourRem12.eq(0))
+ .use(12)
+ .elseUse(hourRem12)
+ .format(intFormatter)
+ }
+
+ @RequiresSchemaVersion(major = 1, minor = 300)
+ private fun buildMinuteInHour(instant: DynamicInstant): DynamicString =
+ instant.getMinute(timeZone).format(intFormatter)
+
+ // NOTE: This dynamic part ignores length.
+ @RequiresSchemaVersion(major = 1, minor = 300)
+ private fun buildAmPmMarker(instant: DynamicInstant): DynamicString {
+ // Using SimpleDateFormat to determine what AM/PM formats to in the given locale.
+ val simpleDateFormat =
+ SimpleDateFormat("a", _locale).also { it.timeZone = TimeZone.getTimeZone("UTC") }
+ // Epoch is AM in UTC.
+ val am = simpleDateFormat.format(Date.from(Instant.EPOCH))
+ // Epoch + 12h is PM in UTC.
+ val pm = simpleDateFormat.format(Date.from(Instant.EPOCH.plus(Duration.ofHours(12))))
+
+ return DynamicString.onCondition(instant.getHour(timeZone).lt(12)).use(am).elseUse(pm)
+ }
+
+ /** Returns a formatter based on the desired [length]. */
+ private val intFormatter
+ @RequiresSchemaVersion(major = 1, minor = 200)
+ get() = DynamicInt32.IntFormatter.Builder().setMinIntegerDigits(length).build()
+ }
+
+ /**
+ * Tokenizes the characters of the string, by replacing every double quotes (`''`) with
+ * [LiteralQuoteToken] and everything else with [CharToken].
+ */
+ private fun String.tokenize(): ArrayDeque<Token> =
+ split("''")
+ .asSequence()
+ .flatMap { it.map(::CharToken) + LiteralQuoteToken }
+ .toCollection(ArrayDeque())
+ .also { it.removeLast() }
+
+ /** Either a "normal" character or an escaped quote (`'`). */
+ private sealed interface Token {
+ /** The value of the token, which should be used based on [isUnescapedQuote]. */
+ val value: Char
+
+ /** Whether token is an unescaped quote (a single `'`). */
+ val isUnescapedQuote: Boolean
+
+ /** Whether the token is a constant (vs dynamic). */
+ val isConstant: Boolean
+ }
+
+ /** A non-literal token that can be a quote (`'`), a constant ([^A-Za-z]), or a pattern. */
+ private data class CharToken(override val value: Char) : Token {
+ override val isUnescapedQuote: Boolean = value == '\''
+ override val isConstant: Boolean =
+ !isUnescapedQuote && value !in 'a'..'z' && value !in 'A'..'Z'
+ }
+
+ /** An escaped quote (`''` that is formatted as a single `'`). */
+ private object LiteralQuoteToken : Token {
+ override val value = '\''
+ override val isUnescapedQuote = false // It's an escaped quote.
+ override val isConstant = true
+ }
+
+ private fun List<Token>.asString() = map { it.value }.joinToString("")
+}
+
+/** In-place equivalent of [ArrayDeque.drop], that accepts a count. */
+private fun <T : Any> ArrayDeque<T>.removeFirst(n: Int) {
+ repeat(n) { removeFirst() }
+}
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/util/DynamicFormatter.kt b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/util/DynamicFormatter.kt
index 66e3d71..2b520e6 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/util/DynamicFormatter.kt
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/util/DynamicFormatter.kt
@@ -38,23 +38,6 @@
*
* See [Formatter] documentation for the format string syntax.
*
- * Example usage:
- * ```
- * DynamicFormatter().format(
- * "%s has walked %d steps. %1$s has also walked %.2f meters.",
- * "John", PlatformHealthSources.dailySteps(), PlatformHealthSources.dailyDistanceMeters()
- * )
- * // Generates an equivalent of:
- * DynamicString.constant("John has walked ")
- * .concat(PlatformHealthSources.dailySteps().format())
- * .concat(DynamicString.constant(" steps. John has also walked "))
- * .concat(
- * PlatformHealthSources.dailyMeters()
- * .format(FloatFormatter.Builder().setMaxFractionDigits(2).build())
- * )
- * .concat(DynamicString.constant(" meters."))
- * ```
- *
* Argument index options (`%s`, `%2$s`, and `%<s`) are fully supported.
*
* These are the supported conversions and options:
@@ -71,12 +54,34 @@
* |%f |See [Formatter] |No |Yes, width and precision|Yes, width and precision|No |No |
* |...|See [Formatter] |No |No |No |No |No |
*
- * NOTE: `%f` has a default precision of 6..6 in [Formatter], which is different from
+ * NOTE 1: `%f` has a default precision of 6..6 in [Formatter], which is different from
* [DynamicFloat.format] which defaults to 0..3. [DynamicFormatter] behaves like [Formatter] and
* defaults to 6..6.
+ *
+ * NOTE 2: [DynamicFormatter] uses `Locale.getDefault(Locale.Category.FORMAT)` at the time of
+ * calling [format] for non-[DynamicType] arguments. This can change on the remote side which, would
+ * cause a mismatch between the locally-formatted arguments and the remotely-formatted arguments,
+ * unless the provider sends a newly formatted [DynamicString] (using a new invocation of [format]).
+ *
+ * Example usage:
+ * ```
+ * DynamicFormatter().format(
+ * "%s has walked %d steps. %1$s has also walked %.2f meters.",
+ * "John", PlatformHealthSources.dailySteps(), PlatformHealthSources.dailyDistanceMeters()
+ * )
+ * // Generates an equivalent of:
+ * DynamicString.constant("John has walked ")
+ * .concat(PlatformHealthSources.dailySteps().format())
+ * .concat(DynamicString.constant(" steps. John has also walked "))
+ * .concat(
+ * PlatformHealthSources.dailyMeters()
+ * .format(FloatFormatter.Builder().setMaxFractionDigits(2).build())
+ * )
+ * .concat(DynamicString.constant(" meters."))
+ * ```
*/
public class DynamicFormatter {
- // TODO: b/297323092 - Allow providing locale for remote evaluation in the constructor.
+ // TODO: b/297323092 - Allow providing in the constructor for both local and remote evaluation.
private val locale
get() = Locale.getDefault(Locale.Category.FORMAT)
@@ -124,39 +129,38 @@
* on the result.
*/
@RequiresSchemaVersion(major = 1, minor = 200)
- private fun extractFormatParts(
- format: String,
- args: Array<out Any?>
- ): Sequence<ConstantOrDynamicPart> = sequence {
- var lastPosition = 0
- var lastVariableIndex = -1
- var lastPositionalVariableIndex = -1
- for (match in PATTERN.findAll(format)) {
- if (match.range.first > lastPosition) {
- // Non-variable (non-match) from the end of the last dynamic part, i.e.:
- // "...<dynamic><*constant*><dynamic>...".
- yield(ConstantPart(format.substring(lastPosition until match.range.first)))
+ private fun extractFormatParts(format: String, args: Array<out Any?>): Sequence<Part> =
+ sequence {
+ var lastPosition = 0
+ var lastVariableIndex = -1
+ var lastPositionalVariableIndex = -1
+ for (match in PATTERN.findAll(format)) {
+ if (match.range.first > lastPosition) {
+ // Non-variable (non-match) from the end of the last dynamic part, i.e.:
+ // "...<dynamic><*constant*><dynamic>...".
+ yield(Part.Constant(format.substring(lastPosition until match.range.first)))
+ }
+ // Variable match - parsing the specifier, maintaining last indices, and formatting.
+ val formatAttributes =
+ match.toFormatStringVariable(
+ lastIndex = lastVariableIndex,
+ lastPositionalIndex = lastPositionalVariableIndex
+ )
+ lastVariableIndex = formatAttributes.index
+ if (formatAttributes.isPositionalIndex) {
+ lastPositionalVariableIndex = formatAttributes.index
+ }
+ yield(formatAttributes.format(args))
+ // Remembering position in order to extract non-variable parts between variable
+ // matches.
+ lastPosition = match.range.last + 1
}
- // Variable match - parsing the specifier, maintaining last indices, and formatting.
- val formatAttributes =
- match.toFormatStringVariable(
- lastIndex = lastVariableIndex,
- lastPositionalIndex = lastPositionalVariableIndex
- )
- lastVariableIndex = formatAttributes.index
- if (formatAttributes.isPositionalIndex) {
- lastPositionalVariableIndex = formatAttributes.index
+ if (lastPosition < format.length) {
+ // Non-variable (non-match) from the end of the last dynamic part at the end of the
+ // format, i.e.: "...<dynamic><*constant*>".
+ yield(Part.Constant(format.substring(lastPosition)))
}
- yield(formatAttributes.format(args))
- // Remembering position in order to extract non-variable parts between variable matches.
- lastPosition = match.range.last + 1
}
- if (lastPosition < format.length) {
- // Non-variable (non-match) from the end of the last dynamic part at the end of the
- // format, i.e.: "...<dynamic><*constant*>".
- yield(ConstantPart(format.substring(lastPosition)))
- }
- }
/**
* Converts a [PATTERN] match to an [FormatStringVariable].
@@ -228,36 +232,35 @@
* )
* ```
*/
- private fun Sequence<ConstantOrDynamicPart>.mergeConstants(): Sequence<ConstantOrDynamicPart> =
- sequence {
- val empty = ConstantPart("")
- var lastConstant = empty
- forEach { nextSection ->
- if (nextSection is ConstantPart) {
- lastConstant += nextSection
- } else {
- if (lastConstant.value.isNotEmpty()) yield(lastConstant)
- lastConstant = empty
- yield(nextSection)
- }
+ private fun Sequence<Part>.mergeConstants(): Sequence<Part> = sequence {
+ val empty = Part.Constant("")
+ var lastConstant = empty
+ forEach { nextSection ->
+ if (nextSection is Part.Constant) {
+ lastConstant += nextSection
+ } else {
+ if (lastConstant.value.isNotEmpty()) yield(lastConstant)
+ lastConstant = empty
+ yield(nextSection)
}
- if (lastConstant.value.isNotEmpty()) yield(lastConstant)
+ }
+ if (lastConstant.value.isNotEmpty()) yield(lastConstant)
+ }
+
+ /** Represents a [Constant] or a [Dynamic] value. */
+ private sealed interface Part {
+ @RequiresSchemaVersion(major = 1, minor = 200) fun toDynamicString(): DynamicString
+
+ data class Constant(val value: String) : Part {
+ operator fun plus(other: Constant) = Constant(value + other.value)
+
+ @RequiresSchemaVersion(major = 1, minor = 200)
+ override fun toDynamicString() = DynamicString.constant(value)
}
- /** Represents a [ConstantPart] or a [DynamicPart] value. */
- private sealed interface ConstantOrDynamicPart {
- @RequiresSchemaVersion(major = 1, minor = 200) fun toDynamicString(): DynamicString
- }
-
- private data class ConstantPart(val value: String) : ConstantOrDynamicPart {
- operator fun plus(other: ConstantPart) = ConstantPart(value + other.value)
-
- @RequiresSchemaVersion(major = 1, minor = 200)
- override fun toDynamicString() = DynamicString.constant(value)
- }
-
- private data class DynamicPart(val value: DynamicString) : ConstantOrDynamicPart {
- @RequiresSchemaVersion(major = 1, minor = 200) override fun toDynamicString() = value
+ data class Dynamic(val value: DynamicString) : Part {
+ @RequiresSchemaVersion(major = 1, minor = 200) override fun toDynamicString() = value
+ }
}
/**
@@ -299,17 +302,17 @@
* [DynamicFormatter.format].
*/
@RequiresSchemaVersion(major = 1, minor = 200)
- fun format(args: Array<out Any?>): ConstantOrDynamicPart {
+ fun format(args: Array<out Any?>): Part {
if (index >= args.size) {
throw MissingFormatArgumentException("Format specifier '$specifier'")
}
val arg = args[index]
// Non-DynamicType arguments use Formatter.
- if (arg !is DynamicType) return ConstantPart(arg.defaultFormat())
+ if (arg !is DynamicType) return Part.Constant(arg.defaultFormat())
throwIfIfNotAllowed(arg)
return when (conversion) {
- '%' -> ConstantPart(arg.defaultFormat()) // Argument is ignored by %%.
- 'n' -> ConstantPart(arg.defaultFormat()) // Argument is ignored by %n.
+ '%' -> Part.Constant(arg.defaultFormat()) // Argument is ignored by %%.
+ 'n' -> Part.Constant(arg.defaultFormat()) // Argument is ignored by %n.
's' -> asStringPart(arg)
'S' -> asStringUpperPart(arg)
'b' -> asBooleanPart(arg)
@@ -324,25 +327,25 @@
}
@RequiresSchemaVersion(major = 1, minor = 200)
- private fun asStringPart(arg: DynamicType): ConstantOrDynamicPart =
+ private fun asStringPart(arg: DynamicType): Part =
when (arg) {
is DynamicString -> {
throwUnsupportedForAnyOption()
- DynamicPart(arg)
+ Part.Dynamic(arg)
}
is DynamicInt32 -> {
throwUnsupportedForAnyOption()
- DynamicPart(arg.format())
+ Part.Dynamic(arg.format())
}
is DynamicFloat -> {
throwUnsupportedForAnyOption()
- DynamicPart(
+ Part.Dynamic(
arg.format(FloatFormatter.Builder().setMinFractionDigits(1).build())
)
}
is DynamicBool -> {
throwUnsupportedForAnyOption()
- DynamicPart(
+ Part.Dynamic(
DynamicString.onCondition(arg)
.use(true.defaultFormat())
.elseUse(false.defaultFormat())
@@ -352,21 +355,21 @@
}
@RequiresSchemaVersion(major = 1, minor = 200)
- private fun asStringUpperPart(arg: DynamicType): ConstantOrDynamicPart =
+ private fun asStringUpperPart(arg: DynamicType): Part =
when (arg) {
is DynamicInt32 -> {
throwUnsupportedForAnyOption()
- DynamicPart(arg.format())
+ Part.Dynamic(arg.format())
}
is DynamicFloat -> {
throwUnsupportedForAnyOption()
- DynamicPart(
+ Part.Dynamic(
arg.format(FloatFormatter.Builder().setMinFractionDigits(1).build())
)
}
is DynamicBool -> {
throwUnsupportedForAnyOption()
- DynamicPart(
+ Part.Dynamic(
DynamicString.onCondition(arg)
.use(true.defaultFormat())
.elseUse(false.defaultFormat())
@@ -376,49 +379,49 @@
}
@RequiresSchemaVersion(major = 1, minor = 200)
- private fun asBooleanPart(arg: DynamicType): ConstantOrDynamicPart =
+ private fun asBooleanPart(arg: DynamicType): Part =
when (arg) {
is DynamicBool -> {
throwUnsupportedForAnyOption()
- DynamicPart(
+ Part.Dynamic(
DynamicString.onCondition(arg)
.use(true.defaultFormat())
.elseUse(false.defaultFormat())
)
}
// All non-null is true, including DynamicType.
- else -> ConstantPart(arg.defaultFormat())
+ else -> Part.Constant(arg.defaultFormat())
}
@RequiresSchemaVersion(major = 1, minor = 200)
- private fun asBooleanUpperPart(arg: DynamicType): ConstantOrDynamicPart =
+ private fun asBooleanUpperPart(arg: DynamicType): Part =
when (arg) {
is DynamicBool -> {
throwUnsupportedForAnyOption()
- DynamicPart(DynamicString.onCondition(arg).use("TRUE").elseUse("FALSE"))
+ Part.Dynamic(DynamicString.onCondition(arg).use("TRUE").elseUse("FALSE"))
}
// All non-null is true, including DynamicType.
- else -> ConstantPart(arg.defaultFormat())
+ else -> Part.Constant(arg.defaultFormat())
}
@RequiresSchemaVersion(major = 1, minor = 200)
- private fun asDecimalPart(arg: DynamicType): ConstantOrDynamicPart =
+ private fun asDecimalPart(arg: DynamicType): Part =
when (arg) {
is DynamicInt32 -> {
throwUnsupportedForAnyOption()
- DynamicPart(arg.format())
+ Part.Dynamic(arg.format())
}
else -> throwUnsupportedDynamicType(arg)
}
@RequiresSchemaVersion(major = 1, minor = 200)
- private fun asFloatPart(arg: DynamicType): ConstantOrDynamicPart =
+ private fun asFloatPart(arg: DynamicType): Part =
when (arg) {
is DynamicFloat -> {
throwUnsupportedSpecifierIf(
flags.isNotEmpty() || width != null || dateTimePrefix != null
)
- DynamicPart(
+ Part.Dynamic(
arg.format(
FloatFormatter.Builder()
.apply {
diff --git a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/util/DynamicDateFormatTest.kt b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/util/DynamicDateFormatTest.kt
new file mode 100644
index 0000000..235541c
--- /dev/null
+++ b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/util/DynamicDateFormatTest.kt
@@ -0,0 +1,330 @@
+/*
+ * 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.expression.util
+
+import android.icu.text.SimpleDateFormat
+import android.icu.util.TimeZone
+import android.icu.util.ULocale
+import android.text.format.DateFormat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicInstant
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString
+import androidx.wear.protolayout.expression.pipeline.DynamicTypeBindingRequest
+import androidx.wear.protolayout.expression.pipeline.DynamicTypeEvaluator
+import androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver
+import com.google.common.truth.Expect
+import com.google.common.truth.Truth.assertWithMessage
+import java.time.Duration
+import java.time.Instant
+import java.time.ZoneId
+import java.util.Date
+import java.util.Locale
+import kotlin.test.assertFailsWith
+import kotlin.time.Duration.Companion.hours
+import kotlin.time.Duration.Companion.minutes
+import kotlin.time.toJavaDuration
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.ParameterizedRobolectricTestRunner
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameters
+
+@RunWith(AndroidJUnit4::class)
+class DynamicDateFormatTest {
+ // Using expectation loops instead of parameterized tests because there are too many tests (note
+ // the multiplication in all available locales), too expensive with test infra boilerplate.
+ @get:Rule val expect = Expect.create()
+
+ enum class Case(
+ val pattern: String,
+ val instant: Instant,
+ val expectedEnglish: String,
+ ) {
+ EMPTY(
+ pattern = "",
+ instant = MIDNIGHT,
+ expectedEnglish = "",
+ ),
+ // Constant patterns.
+ CONSTANT_AT_START(
+ pattern = ":::h",
+ instant = MIDNIGHT,
+ expectedEnglish = ":::12",
+ ),
+ CONSTANT_AT_MIDDLE(
+ pattern = "h:::h",
+ instant = MIDNIGHT,
+ expectedEnglish = "12:::12",
+ ),
+ CONSTANT_AT_END(
+ pattern = "h:::",
+ instant = MIDNIGHT,
+ expectedEnglish = "12:::",
+ ),
+ // Escape patterns.
+ ESCAPED_AT_START(
+ pattern = "'Time is' h",
+ instant = MIDNIGHT.plus((3.hours + 20.minutes).toJavaDuration()),
+ expectedEnglish = "Time is 3",
+ ),
+ ESCAPED_AT_MIDDLE(
+ pattern = "h 'h' m",
+ instant = MIDNIGHT.plus((3.hours + 20.minutes).toJavaDuration()),
+ expectedEnglish = "3 h 20",
+ ),
+ ESCAPED_AT_END(
+ pattern = "h 'is the time'",
+ instant = MIDNIGHT.plus((3.hours + 20.minutes).toJavaDuration()),
+ expectedEnglish = "3 is the time",
+ ),
+ ESCAPED_QUOTE_AT_START(
+ pattern = "''h",
+ instant = MIDNIGHT.plus((3.hours + 20.minutes).toJavaDuration()),
+ expectedEnglish = "'3",
+ ),
+ ESCAPED_QUOTE_AT_MIDDLE(
+ pattern = "h''m",
+ instant = MIDNIGHT.plus((3.hours + 20.minutes).toJavaDuration()),
+ expectedEnglish = "3'20",
+ ),
+ ESCAPED_QUOTE_AT_END(
+ pattern = "h''",
+ instant = MIDNIGHT.plus((3.hours + 20.minutes).toJavaDuration()),
+ expectedEnglish = "3'",
+ ),
+ ESCAPED_QUOTE_IN_ESCAPED(
+ pattern = "'Time''s' h",
+ instant = MIDNIGHT.plus((3.hours + 20.minutes).toJavaDuration()),
+ expectedEnglish = "Time's 3",
+ ),
+ // Hours patterns.
+ HOURS_PADDED_MIDNIGHT(
+ pattern = "HHHH:KKKK:hhhh:kkkk:aaaa",
+ instant = MIDNIGHT,
+ expectedEnglish = "0000:0000:0012:0024:AM",
+ ),
+ HOURS_PADDED_NOON(
+ pattern = "HHHH:KKKK:hhhh:kkkk:aaaa",
+ instant = MIDNIGHT.plus(Duration.ofHours(12)),
+ expectedEnglish = "0012:0000:0012:0012:PM",
+ ),
+ HOURS_PADDED_A_M(
+ pattern = "HHHH:KKKK:hhhh:kkkk:aaaa",
+ instant = MIDNIGHT.plus(Duration.ofHours(6)),
+ expectedEnglish = "0006:0006:0006:0006:AM",
+ ),
+ HOURS_PADDED_P_M(
+ pattern = "H:K:h:k:a",
+ instant = MIDNIGHT.plus(Duration.ofHours(18)),
+ expectedEnglish = "18:6:6:18:PM",
+ ),
+ HOURS_NOT_PADDED_MIDNIGHT(
+ pattern = "H:K:h:k:a",
+ instant = MIDNIGHT,
+ expectedEnglish = "0:0:12:24:AM",
+ ),
+ HOURS_NOT_PADDED_NOON(
+ pattern = "H:K:h:k:a",
+ instant = MIDNIGHT.plus(Duration.ofHours(12)),
+ expectedEnglish = "12:0:12:12:PM",
+ ),
+ HOURS_NOT_PADDED_A_M(
+ pattern = "H:K:h:k:a",
+ instant = MIDNIGHT.plus(Duration.ofHours(6)),
+ expectedEnglish = "6:6:6:6:AM",
+ ),
+ HOURS_NOT_PADDED_P_M(
+ pattern = "H:K:h:k:a",
+ instant = MIDNIGHT.plus(Duration.ofHours(18)),
+ expectedEnglish = "18:6:6:18:PM",
+ ),
+ // Minute patterns.
+ MINUTES_PADDED_ZERO(
+ pattern = "mmmm",
+ instant = MIDNIGHT,
+ expectedEnglish = "0000",
+ ),
+ MINUTES_PADDED_SINGLE_DIGIT(
+ pattern = "mmmm",
+ instant = MIDNIGHT.plus(Duration.ofMinutes(5)),
+ expectedEnglish = "0005",
+ ),
+ MINUTES_PADDED_DOUBLE_DIGIT(
+ pattern = "mmmm",
+ instant = MIDNIGHT.plus(Duration.ofMinutes(15)),
+ expectedEnglish = "0015",
+ ),
+ MINUTES_NOT_PADDED_ZERO(
+ pattern = "m",
+ instant = MIDNIGHT,
+ expectedEnglish = "0",
+ ),
+ MINUTES_NOT_PADDED_SINGLE_DIGIT(
+ pattern = "m",
+ instant = MIDNIGHT.plus(Duration.ofMinutes(5)),
+ expectedEnglish = "5",
+ ),
+ MINUTES_NOT_PADDED_DOUBLE_DIGIT(
+ pattern = "m",
+ instant = MIDNIGHT.plus(Duration.ofMinutes(15)),
+ expectedEnglish = "15",
+ ),
+ }
+
+ @Test
+ fun matchesExpectedEnglish() {
+ for (case in Case.values()) {
+ expectFormatsTo(
+ message = case.name,
+ pattern = case.pattern,
+ instant = case.instant,
+ locale = EN,
+ expected = case.expectedEnglish,
+ )
+ }
+ }
+
+ /**
+ * Tests that [Case.pattern] formats to the same value [SimpleDateFormat] does, in all available
+ * locales.
+ */
+ @Test
+ fun matchesSimpleDateFormat() {
+ for (case in Case.values()) {
+ for (locale in Locale.getAvailableLocales()) {
+ expectMatchesSimpleDateFormat(
+ message = "${case.name}_$locale",
+ pattern = case.pattern,
+ instant = case.instant,
+ locale = locale,
+ )
+ }
+ }
+ }
+
+ /**
+ * Tests that the best date time pattern for "hmm" matches [SimpleDateFormat], in all available
+ * locales.
+ */
+ @Test
+ fun bestDateTimePattern_matchesSimpleDateFormat() {
+ for (locale in Locale.getAvailableLocales()) {
+ val pattern = DateFormat.getBestDateTimePattern(locale, "hmm")
+ expectMatchesSimpleDateFormat(
+ message = "${pattern}_$locale",
+ pattern = pattern,
+ instant = MIDNIGHT,
+ locale = locale,
+ )
+ }
+ }
+
+ /** Expects the [pattern] formats to [expected], in the given [locale] and [instant]. */
+ private fun expectFormatsTo(
+ message: String,
+ pattern: String,
+ instant: Instant,
+ locale: Locale,
+ expected: String,
+ ) {
+ val formatter = DynamicDateFormat(pattern, locale, TIME_ZONE)
+
+ val actual: String =
+ formatter.format(DynamicInstant.withSecondsPrecision(instant)).evaluate(locale)
+
+ expect.withMessage(message).that(actual).isEqualTo(expected)
+ }
+
+ /**
+ * Expects the [pattern] formats to the same value [SimpleDateFormat] does, in the given
+ * [locale] and [instant].
+ */
+ private fun expectMatchesSimpleDateFormat(
+ message: String,
+ pattern: String,
+ instant: Instant,
+ locale: Locale,
+ ) {
+ expectFormatsTo(
+ message = message,
+ pattern = pattern,
+ instant = instant,
+ locale = locale,
+ expected =
+ SimpleDateFormat(pattern, locale)
+ .also { it.timeZone = TimeZone.getTimeZone(TIME_ZONE.id) }
+ .format(Date.from(instant))
+ )
+ }
+
+ /** Synchronously evaluates the [DynamicString] using [DynamicTypeEvaluator]. */
+ private fun DynamicString.evaluate(locale: Locale): String {
+ lateinit var result: String
+ val evaluator = DynamicTypeEvaluator(DynamicTypeEvaluator.Config.Builder().build())
+ evaluator
+ .bind(
+ DynamicTypeBindingRequest.forDynamicString(
+ this@evaluate,
+ ULocale.forLocale(locale),
+ { it.run() }, // Synchronous executor
+ object : DynamicTypeValueReceiver<String> {
+ override fun onData(newData: String) {
+ result = newData
+ }
+
+ override fun onInvalidated() {
+ throw AssertionError("DynamicString invalidated: ${this@evaluate}")
+ }
+ }
+ )
+ )
+ .startEvaluation()
+ return result
+ }
+
+ companion object {
+ private val TIME_ZONE = ZoneId.of("UTC")
+ private val MIDNIGHT = Instant.EPOCH // Epoch is midnight in UTC.
+ private val EN = Locale.ENGLISH
+ }
+}
+
+@RunWith(ParameterizedRobolectricTestRunner::class)
+class DynamicDateFormatFailingTest(private val case: Case) {
+ enum class Case(val expected: Exception, val pattern: String) {
+ UNSUPPORTED_LETTER(
+ IllegalArgumentException("Illegal pattern character 'y'"),
+ "hh:mm yyyy-MM-dd"
+ ),
+ ODD_QUOTES(IllegalArgumentException("Unterminated quote"), "'''"),
+ }
+
+ @Test
+ fun fails() {
+ val exception =
+ assertFailsWith(case.expected::class, case.name) { DynamicDateFormat(case.pattern) }
+
+ assertWithMessage(case.name)
+ .that(exception)
+ .hasMessageThat()
+ .isEqualTo(case.expected.message)
+ }
+
+ companion object {
+ @Parameters @JvmStatic fun parameters() = Case.values()
+ }
+}
diff --git a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/util/DynamicFormatterTest.kt b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/util/DynamicFormatterTest.kt
index bec01a3..0bb33ca 100644
--- a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/util/DynamicFormatterTest.kt
+++ b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/util/DynamicFormatterTest.kt
@@ -26,7 +26,7 @@
import androidx.wear.protolayout.expression.pipeline.DynamicTypeBindingRequest
import androidx.wear.protolayout.expression.pipeline.DynamicTypeEvaluator
import androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver
-import com.google.common.truth.Truth.assertWithMessage
+import com.google.common.truth.Truth.assertThat
import java.time.Instant
import java.util.IllegalFormatConversionException
import java.util.MissingFormatArgumentException
@@ -39,166 +39,169 @@
@RunWith(ParameterizedRobolectricTestRunner::class)
class DynamicFormatterFormatTest(private val case: Case) {
enum class Case(
- val expected: String,
val format: String,
val args: List<Any?>? = null,
val dynamicArgs: List<DynamicType>? = null,
val equivalentArgs: List<Any?>? = null,
+ val expected: String,
) {
- CONSTANT_ONLY("hello world", "hello world", args = listOf()),
- NON_DYNAMIC_ARGS("hello world", "hello %s", args = listOf("world")),
- EXPLICIT_ARG_INDEX("12 34 56 56", "%d %3\$d %d %<d", args = listOf(12, 56, 34)),
+ CONSTANT_ONLY(format = "hello world", args = listOf(), expected = "hello world"),
+ NON_DYNAMIC_ARGS(format = "hello %s", args = listOf("world"), expected = "hello world"),
+ EXPLICIT_ARG_INDEX(
+ format = "%d %3\$d %d %<d",
+ args = listOf(12, 56, 34),
+ expected = "12 34 56 56",
+ ),
DYNAMIC_START(
- "hello world",
- "%s world",
+ format = "%s world",
dynamicArgs = listOf(DynamicString.constant("hello")),
equivalentArgs = listOf("hello"),
+ expected = "hello world",
),
DYNAMIC_MIDDLE(
- "hello world",
- "hel%srld",
+ format = "hel%srld",
dynamicArgs = listOf(DynamicString.constant("lo wo")),
equivalentArgs = listOf("lo wo"),
+ expected = "hello world",
),
DYNAMIC_END(
- "hello world",
- "hello %s",
+ format = "hello %s",
dynamicArgs = listOf(DynamicString.constant("world")),
equivalentArgs = listOf("world"),
+ expected = "hello world",
),
SEPARATED_DYNAMIC(
- "hello world",
- "%s %s",
+ format = "%s %s",
dynamicArgs = listOf(DynamicString.constant("hello"), DynamicString.constant("world")),
equivalentArgs = listOf("hello", "world"),
+ expected = "hello world",
),
CONNECTED_DYNAMIC(
- "helloworld",
- "%s%s",
+ format = "%s%s",
dynamicArgs = listOf(DynamicString.constant("hello"), DynamicString.constant("world")),
equivalentArgs = listOf("hello", "world"),
+ expected = "helloworld",
),
// %%
FORMAT_percent_DYNAMIC_TYPE(
- "%",
- "%%",
+ format = "%%",
// args are ignored
dynamicArgs = listOf(DynamicString.constant("hello")),
equivalentArgs = listOf("hello"),
+ expected = "%",
),
// %%
FORMAT_n_DYNAMIC_TYPE(
- "\n",
- "%n",
+ format = "%n",
// args are ignored
dynamicArgs = listOf(DynamicString.constant("hello")),
equivalentArgs = listOf("hello"),
+ expected = "\n",
),
// %s
FORMAT_s_DYNAMIC_STRING(
- "ab",
- "%s",
+ format = "%s",
dynamicArgs = listOf(DynamicString.constant("ab")),
equivalentArgs = listOf("ab"),
+ expected = "ab",
),
FORMAT_s_DYNAMIC_INT32(
- "12",
- "%s",
+ format = "%s",
dynamicArgs = listOf(DynamicInt32.constant(12)),
equivalentArgs = listOf(12),
+ expected = "12",
),
FORMAT_s_DYNAMIC_FLOAT(
- "12.0",
- "%s",
+ format = "%s",
dynamicArgs = listOf(DynamicFloat.constant(12f)),
equivalentArgs = listOf(12f),
+ expected = "12.0",
),
FORMAT_s_DYNAMIC_BOOL_TRUE(
- "true",
- "%s",
+ format = "%s",
dynamicArgs = listOf(DynamicBool.constant(true)),
equivalentArgs = listOf(true),
+ expected = "true",
),
FORMAT_s_DYNAMIC_BOOL_FALSE(
- "false",
- "%s",
+ format = "%s",
dynamicArgs = listOf(DynamicBool.constant(false)),
equivalentArgs = listOf(false),
+ expected = "false",
),
// %S
FORMAT_S_DYNAMIC_INT32(
- "12",
- "%S",
+ format = "%S",
dynamicArgs = listOf(DynamicInt32.constant(12)),
equivalentArgs = listOf(12),
+ expected = "12",
),
FORMAT_S_DYNAMIC_FLOAT(
- "12.0",
- "%S",
+ format = "%S",
dynamicArgs = listOf(DynamicFloat.constant(12f)),
equivalentArgs = listOf(12f),
+ expected = "12.0",
),
FORMAT_S_DYNAMIC_BOOL_TRUE(
- "TRUE",
- "%S",
+ format = "%S",
dynamicArgs = listOf(DynamicBool.constant(true)),
equivalentArgs = listOf(true),
+ expected = "TRUE",
),
FORMAT_S_DYNAMIC_BOOL_FALSE(
- "FALSE",
- "%S",
+ format = "%S",
dynamicArgs = listOf(DynamicBool.constant(false)),
equivalentArgs = listOf(false),
+ expected = "FALSE",
),
// %b
FORMAT_b_DYNAMIC_BOOL_TRUE(
- "true",
- "%b",
+ format = "%b",
dynamicArgs = listOf(DynamicBool.constant(true)),
equivalentArgs = listOf(true),
+ expected = "true",
),
FORMAT_b_DYNAMIC_BOOL_FALSE(
- "false",
- "%b",
+ format = "%b",
dynamicArgs = listOf(DynamicBool.constant(false)),
equivalentArgs = listOf(false),
+ expected = "false",
),
FORMAT_b_DYNAMIC_TYPE_IS_TRUE(
- "true",
- "%b",
+ format = "%b",
dynamicArgs = listOf(DYNAMIC_INSTANT),
equivalentArgs = listOf(INSTANT),
+ expected = "true",
),
// %B
FORMAT_B_DYNAMIC_BOOL_TRUE(
- "TRUE",
- "%B",
+ format = "%B",
dynamicArgs = listOf(DynamicBool.constant(true)),
equivalentArgs = listOf(true),
+ expected = "TRUE",
),
FORMAT_B_DYNAMIC_BOOL_FALSE(
- "FALSE",
- "%B",
+ format = "%B",
dynamicArgs = listOf(DynamicBool.constant(false)),
equivalentArgs = listOf(false),
+ expected = "FALSE",
),
FORMAT_B_DYNAMIC_TYPE_IS_TRUE(
- "TRUE",
- "%B",
+ format = "%B",
dynamicArgs = listOf(DYNAMIC_INSTANT),
equivalentArgs = listOf(INSTANT),
+ expected = "TRUE",
),
// %d
FORMAT_d_DYNAMIC_INT32(
- "12",
- "%d",
+ format = "%d",
dynamicArgs = listOf(DynamicInt32.constant(12)),
equivalentArgs = listOf(12),
+ expected = "12",
),
// %f
FORMAT_f_DYNAMIC_FLOAT(
- "12.345000 34.57 56.70",
- "%f %.2f %.2f",
+ format = "%f %.2f %.2f",
dynamicArgs =
listOf(
DynamicFloat.constant(12.345f), // default fraction digits
@@ -206,23 +209,21 @@
DynamicFloat.constant(56.7f), // min fraction digits
),
equivalentArgs = listOf(12.345f, 34.567f, 56.7f),
+ expected = "12.345000 34.57 56.70",
),
}
@Test
fun equalsExpected() {
val args = (case.args ?: case.dynamicArgs)!!.toTypedArray()
- assertWithMessage(case.name)
- .that(DynamicString.format(case.format, *args).evaluate())
- .isEqualTo(case.expected)
+ assertThat(DynamicString.format(case.format, *args).evaluate()).isEqualTo(case.expected)
}
@Test
fun equalsDefault() {
val dynamicArgs = (case.args ?: case.dynamicArgs)!!.toTypedArray()
val equivalentArgs = (case.args ?: case.equivalentArgs)!!.toTypedArray()
- assertWithMessage(case.name)
- .that(DynamicString.format(case.format, *dynamicArgs).evaluate())
+ assertThat(DynamicString.format(case.format, *dynamicArgs).evaluate())
.isEqualTo(case.format.format(*equivalentArgs))
}
@@ -256,115 +257,114 @@
@RunWith(ParameterizedRobolectricTestRunner::class)
class DynamicFormatterFailingTest(private val case: Case) {
- enum class Case(val expected: Exception, val format: String, val args: List<Any?> = listOf()) {
+ enum class Case(val format: String, val args: List<Any?> = listOf(), val expected: Exception) {
NOT_ENOUGH_POSITIONAL_ARGS(
- MissingFormatArgumentException("Format specifier '%s'"),
- "%s %s",
- args = listOf("hello")
+ format = "%s %s",
+ args = listOf("hello"),
+ expected = MissingFormatArgumentException("Format specifier '%s'"),
),
NOT_ENOUGH_EXPLICIT_INDEX_ARGS(
- MissingFormatArgumentException("Format specifier '%3\$s'"),
- "%s %3\$s",
- args = listOf("hello")
+ format = "%s %3\$s",
+ args = listOf("hello"),
+ expected = MissingFormatArgumentException("Format specifier '%3\$s'"),
),
RELATIVE_INDEX_IS_FIRST(
- MissingFormatArgumentException("Format specifier '%<s'"),
- "%<s",
- args = listOf("hello")
+ format = "%<s",
+ args = listOf("hello"),
+ expected = MissingFormatArgumentException("Format specifier '%<s'"),
),
DYNAMIC_ARG_NOT_ALLOWED(
- IllegalFormatConversionException('d', String::class.java),
- "%d",
- args = listOf(DynamicString.constant("hello"))
+ format = "%d",
+ args = listOf(DynamicString.constant("hello")),
+ expected = IllegalFormatConversionException('d', String::class.java),
),
UNSUPPORTED_DYNAMIC_CONVERSION(
- UnsupportedOperationException("Unsupported conversion for DynamicType: 'h'"),
- "%h",
- args = listOf(DynamicInt32.constant(12))
+ format = "%h",
+ args = listOf(DynamicInt32.constant(12)),
+ expected = UnsupportedOperationException("Unsupported conversion for DynamicType: 'h'"),
),
// %s
FORMAT_s_DYNAMIC_STRING_WITH_UNSUPPORTED_OPTIONS(
- UnsupportedOperationException("Unsupported specifier: '%2s'"),
- "%2s",
- args = listOf(DynamicString.constant("a"))
+ format = "%2s",
+ args = listOf(DynamicString.constant("a")),
+ expected = UnsupportedOperationException("Unsupported specifier: '%2s'"),
),
FORMAT_s_DYNAMIC_INT32_WITH_UNSUPPORTED_OPTIONS(
- UnsupportedOperationException("Unsupported specifier: '%2s'"),
- "%2s",
- args = listOf(DynamicInt32.constant(12))
+ format = "%2s",
+ args = listOf(DynamicInt32.constant(12)),
+ expected = UnsupportedOperationException("Unsupported specifier: '%2s'"),
),
FORMAT_s_DYNAMIC_FLOAT_WITH_UNSUPPORTED_OPTIONS(
- UnsupportedOperationException("Unsupported specifier: '%2s'"),
- "%2s",
- args = listOf(DynamicFloat.constant(12f))
+ format = "%2s",
+ args = listOf(DynamicFloat.constant(12f)),
+ expected = UnsupportedOperationException("Unsupported specifier: '%2s'"),
),
FORMAT_s_DYNAMIC_BOOL_WITH_UNSUPPORTED_OPTIONS(
- UnsupportedOperationException("Unsupported specifier: '%2s'"),
- "%2s",
- args = listOf(DynamicBool.constant(true))
+ format = "%2s",
+ args = listOf(DynamicBool.constant(true)),
+ expected = UnsupportedOperationException("Unsupported specifier: '%2s'"),
),
FORMAT_s_UNSUPPORTED_DYNAMIC_ARG(
- UnsupportedOperationException("$DYNAMIC_INSTANT unsupported for specifier: '%s'"),
- "%s",
+ format = "%s",
args = listOf(DYNAMIC_INSTANT),
+ expected =
+ UnsupportedOperationException("$DYNAMIC_INSTANT unsupported for specifier: '%s'"),
),
// %S
FORMAT_S_DYNAMIC_INT32_WITH_UNSUPPORTED_OPTIONS(
- UnsupportedOperationException("Unsupported specifier: '%2S'"),
- "%2S",
- args = listOf(DynamicInt32.constant(12))
+ format = "%2S",
+ args = listOf(DynamicInt32.constant(12)),
+ expected = UnsupportedOperationException("Unsupported specifier: '%2S'"),
),
FORMAT_S_DYNAMIC_FLOAT_WITH_UNSUPPORTED_OPTIONS(
- UnsupportedOperationException("Unsupported specifier: '%2S'"),
- "%2S",
- args = listOf(DynamicFloat.constant(12f))
+ format = "%2S",
+ args = listOf(DynamicFloat.constant(12f)),
+ expected = UnsupportedOperationException("Unsupported specifier: '%2S'"),
),
FORMAT_S_DYNAMIC_BOOL_WITH_UNSUPPORTED_OPTIONS(
- UnsupportedOperationException("Unsupported specifier: '%2S'"),
- "%2S",
- args = listOf(DynamicBool.constant(true))
+ format = "%2S",
+ args = listOf(DynamicBool.constant(true)),
+ expected = UnsupportedOperationException("Unsupported specifier: '%2S'"),
),
FORMAT_S_UNSUPPORTED_DYNAMIC_ARG(
- UnsupportedOperationException("$DYNAMIC_INSTANT unsupported for specifier: '%S'"),
- "%S",
+ format = "%S",
args = listOf(DYNAMIC_INSTANT),
+ expected =
+ UnsupportedOperationException("$DYNAMIC_INSTANT unsupported for specifier: '%S'"),
),
// %b
FORMAT_b_DYNAMIC_BOOL_WITH_UNSUPPORTED_OPTIONS(
- UnsupportedOperationException("Unsupported specifier: '%2b'"),
- "%2b",
- args = listOf(DynamicBool.constant(true))
+ format = "%2b",
+ args = listOf(DynamicBool.constant(true)),
+ expected = UnsupportedOperationException("Unsupported specifier: '%2b'"),
),
// %B
FORMAT_B_DYNAMIC_BOOL_WITH_UNSUPPORTED_OPTIONS(
- UnsupportedOperationException("Unsupported specifier: '%2B'"),
- "%2B",
- args = listOf(DynamicBool.constant(true))
+ format = "%2B",
+ args = listOf(DynamicBool.constant(true)),
+ expected = UnsupportedOperationException("Unsupported specifier: '%2B'"),
),
// %d
FORMAT_d_DYNAMIC_INT32_WITH_UNSUPPORTED_OPTIONS(
- UnsupportedOperationException("Unsupported specifier: '%2d'"),
- "%2d",
- args = listOf(DynamicInt32.constant(12))
+ format = "%2d",
+ args = listOf(DynamicInt32.constant(12)),
+ expected = UnsupportedOperationException("Unsupported specifier: '%2d'"),
),
// %f
FORMAT_f_DYNAMIC_FLOAT_WITH_UNSUPPORTED_OPTIONS(
- UnsupportedOperationException("Unsupported specifier: '%2f'"),
- "%2f",
- args = listOf(DynamicFloat.constant(12f))
+ format = "%2f",
+ args = listOf(DynamicFloat.constant(12f)),
+ expected = UnsupportedOperationException("Unsupported specifier: '%2f'"),
),
}
@Test
fun fails() {
val exception =
- assertFailsWith(case.expected::class, case.name) {
+ assertFailsWith(case.expected::class) {
DynamicString.format(case.format, *case.args.toTypedArray())
}
- assertWithMessage(case.name)
- .that(exception)
- .hasMessageThat()
- .isEqualTo(case.expected.message)
+ assertThat(exception).hasMessageThat().isEqualTo(case.expected.message)
}
companion object {
diff --git a/wear/protolayout/protolayout-lint/src/main/java/androidx/wear/protolayout/lint/ProtoLayoutMinSchemaDetector.kt b/wear/protolayout/protolayout-lint/src/main/java/androidx/wear/protolayout/lint/ProtoLayoutMinSchemaDetector.kt
index 21678fe..0910d32 100644
--- a/wear/protolayout/protolayout-lint/src/main/java/androidx/wear/protolayout/lint/ProtoLayoutMinSchemaDetector.kt
+++ b/wear/protolayout/protolayout-lint/src/main/java/androidx/wear/protolayout/lint/ProtoLayoutMinSchemaDetector.kt
@@ -144,6 +144,7 @@
in 101..200 -> 33
in 201..300 -> 34
in 301..400 -> 35
+ in 401..500 -> 36
else -> Int.MAX_VALUE
}
diff --git a/wear/watchface/watchface-client-guava/build.gradle b/wear/watchface/watchface-client-guava/build.gradle
index a393a6b..310b255 100644
--- a/wear/watchface/watchface-client-guava/build.gradle
+++ b/wear/watchface/watchface-client-guava/build.gradle
@@ -41,8 +41,8 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.truth)
}
diff --git a/wear/watchface/watchface-client/build.gradle b/wear/watchface/watchface-client/build.gradle
index 075f4bfe..f43e366 100644
--- a/wear/watchface/watchface-client/build.gradle
+++ b/wear/watchface/watchface-client/build.gradle
@@ -44,8 +44,8 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.truth)
testImplementation(libs.mockitoCore4)
diff --git a/wear/watchface/watchface-editor-guava/build.gradle b/wear/watchface/watchface-editor-guava/build.gradle
index 6baca89..15c9ff976 100644
--- a/wear/watchface/watchface-editor-guava/build.gradle
+++ b/wear/watchface/watchface-editor-guava/build.gradle
@@ -39,8 +39,8 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.truth)
}
diff --git a/wear/watchface/watchface-editor/build.gradle b/wear/watchface/watchface-editor/build.gradle
index 0bd2f0e..b3747af 100644
--- a/wear/watchface/watchface-editor/build.gradle
+++ b/wear/watchface/watchface-editor/build.gradle
@@ -49,8 +49,8 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.kotlinCoroutinesTest)
androidTestImplementation(libs.kotlinTest)
androidTestImplementation(libs.truth)
diff --git a/wear/watchface/watchface-guava/build.gradle b/wear/watchface/watchface-guava/build.gradle
index 319eb9d..28b9236 100644
--- a/wear/watchface/watchface-guava/build.gradle
+++ b/wear/watchface/watchface-guava/build.gradle
@@ -39,14 +39,14 @@
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.truth)
testImplementation(libs.testCore)
testImplementation(libs.testRunner)
testImplementation(libs.testRules)
- testImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- testImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ testImplementation(libs.mockitoCore)
+ testImplementation(libs.dexmakerMockito)
testImplementation(libs.robolectric)
testImplementation(libs.truth)
}
diff --git a/wear/watchface/watchface/build.gradle b/wear/watchface/watchface/build.gradle
index 8b5f470..2204343 100644
--- a/wear/watchface/watchface/build.gradle
+++ b/wear/watchface/watchface/build.gradle
@@ -48,9 +48,9 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
androidTestImplementation(libs.mockitoKotlin)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.truth)
testImplementation(project(":wear:watchface:watchface-complications-rendering"))
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
index c065779..421dc62 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
@@ -999,7 +999,6 @@
}
if (watchState.isHeadless) {
headlessWatchFaceImpl!!.release()
- [email protected]()
}
}
}
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
index 2c52bc1..12262b6 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
@@ -1927,6 +1927,7 @@
if (this::choreographer.isInitialized) {
frameCallback?.let { choreographer.removeFrameCallback(it) }
}
+ frameCallback = null
if (this::interactiveInstanceId.isInitialized) {
InteractiveInstanceManager.deleteInstance(interactiveInstanceId)
}
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index 28b26e9..80ca709 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -7756,6 +7756,14 @@
) {
// Intentionally empty.
}
+
+ var destroyed: Boolean = false
+
+ override fun onDestroy() {
+ super.onDestroy()
+ assert(!destroyed) { "onDestroy already called!!" }
+ destroyed = true
+ }
}
)
diff --git a/wear/wear/build.gradle b/wear/wear/build.gradle
index 41e2757..bc59963 100644
--- a/wear/wear/build.gradle
+++ b/wear/wear/build.gradle
@@ -30,8 +30,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.truth)
testImplementation(libs.kotlinStdlib)
diff --git a/wear/wear_sdk/README.txt b/wear/wear_sdk/README.txt
index 26122f9..224156b 100644
--- a/wear/wear_sdk/README.txt
+++ b/wear/wear_sdk/README.txt
@@ -5,5 +5,5 @@
"preinstalled on WearOS devices."
gerrit source: "vendor/google_clockwork/sdk/lib"
API version: 35.1
-Build ID: 12239970
-Last updated: Fri Aug 16 06:57:41 PM UTC 2024
+Build ID: 12295646
+Last updated: Thu Aug 29 08:37:30 PM UTC 2024
diff --git a/wear/wear_sdk/wear-sdk.jar b/wear/wear_sdk/wear-sdk.jar
index 36f61d4..43b434d 100644
--- a/wear/wear_sdk/wear-sdk.jar
+++ b/wear/wear_sdk/wear-sdk.jar
Binary files differ
diff --git a/webkit/integration-tests/instrumentation/build.gradle b/webkit/integration-tests/instrumentation/build.gradle
index 289f6b7..377fea1 100644
--- a/webkit/integration-tests/instrumentation/build.gradle
+++ b/webkit/integration-tests/instrumentation/build.gradle
@@ -45,8 +45,8 @@
androidTestImplementation(libs.testRules)
androidTestImplementation("androidx.appcompat:appcompat:1.1.0")
androidTestImplementation("androidx.concurrent:concurrent-futures:1.0.0")
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
// Hamcrest matchers:
androidTestImplementation(libs.bundles.espressoContrib, excludes.espresso)
diff --git a/webkit/integration-tests/testapp/build.gradle b/webkit/integration-tests/testapp/build.gradle
index 6d26540..09d8d9a 100644
--- a/webkit/integration-tests/testapp/build.gradle
+++ b/webkit/integration-tests/testapp/build.gradle
@@ -46,9 +46,8 @@
androidTestImplementation(libs.bundles.espressoContrib, excludes.espresso)
androidTestImplementation(libs.espressoIdlingResource)
androidTestImplementation(libs.espressoWeb, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
}
// We want to publish a release apk of this project for webkit team's use
diff --git a/webkit/webkit/build.gradle b/webkit/webkit/build.gradle
index 193e4b7..8be9b86 100644
--- a/webkit/webkit/build.gradle
+++ b/webkit/webkit/build.gradle
@@ -41,8 +41,8 @@
androidTestImplementation(libs.testRules)
androidTestImplementation("androidx.appcompat:appcompat:1.1.0")
androidTestImplementation("androidx.concurrent:concurrent-futures:1.0.0")
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
// Hamcrest matchers:
androidTestImplementation(libs.bundles.espressoContrib, excludes.espresso)
diff --git a/window/extensions/extensions/build.gradle b/window/extensions/extensions/build.gradle
index 399cc93..2643332 100644
--- a/window/extensions/extensions/build.gradle
+++ b/window/extensions/extensions/build.gradle
@@ -44,8 +44,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.truth)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
+ androidTestImplementation(libs.dexmakerMockito)
+ androidTestImplementation(libs.mockitoCore)
}
androidx {
diff --git a/window/window-core/api/current.txt b/window/window-core/api/current.txt
index 2c8a4d2..8b80f00 100644
--- a/window/window-core/api/current.txt
+++ b/window/window-core/api/current.txt
@@ -32,9 +32,9 @@
method public int getMinWidthDp();
method @Deprecated public androidx.window.core.layout.WindowHeightSizeClass getWindowHeightSizeClass();
method @Deprecated public androidx.window.core.layout.WindowWidthSizeClass getWindowWidthSizeClass();
- method public boolean isAtLeast(int widthDp, int heightDp);
- method public boolean isHeightAtLeast(int heightDp);
- method public boolean isWidthAtLeast(int widthDp);
+ method public boolean isAtLeastBreakpoint(int widthBreakpointDp, int heightBreakpointDp);
+ method public boolean isHeightAtLeastBreakpoint(int heightBreakpointDp);
+ method public boolean isWidthAtLeastBreakpoint(int widthBreakpointDp);
property public final int minHeightDp;
property public final int minWidthDp;
property @Deprecated public final androidx.window.core.layout.WindowHeightSizeClass windowHeightSizeClass;
diff --git a/window/window-core/api/restricted_current.txt b/window/window-core/api/restricted_current.txt
index 2c8a4d2..8b80f00 100644
--- a/window/window-core/api/restricted_current.txt
+++ b/window/window-core/api/restricted_current.txt
@@ -32,9 +32,9 @@
method public int getMinWidthDp();
method @Deprecated public androidx.window.core.layout.WindowHeightSizeClass getWindowHeightSizeClass();
method @Deprecated public androidx.window.core.layout.WindowWidthSizeClass getWindowWidthSizeClass();
- method public boolean isAtLeast(int widthDp, int heightDp);
- method public boolean isHeightAtLeast(int heightDp);
- method public boolean isWidthAtLeast(int widthDp);
+ method public boolean isAtLeastBreakpoint(int widthBreakpointDp, int heightBreakpointDp);
+ method public boolean isHeightAtLeastBreakpoint(int heightBreakpointDp);
+ method public boolean isWidthAtLeastBreakpoint(int widthBreakpointDp);
property public final int minHeightDp;
property public final int minWidthDp;
property @Deprecated public final androidx.window.core.layout.WindowHeightSizeClass windowHeightSizeClass;
diff --git a/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowSizeClass.kt b/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowSizeClass.kt
index 43bef20..847ea73 100644
--- a/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowSizeClass.kt
+++ b/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowSizeClass.kt
@@ -80,25 +80,28 @@
get() = WindowHeightSizeClass.compute(minHeightDp.toFloat())
/**
- * Returns `true` when [widthDp] is greater than or equal to [minWidthDp], `false` otherwise.
+ * Returns `true` when [minWidthDp] is greater than or equal to [widthBreakpointDp], `false`
+ * otherwise.
*/
- fun isWidthAtLeast(widthDp: Int): Boolean {
- return widthDp >= minWidthDp
+ fun isWidthAtLeastBreakpoint(widthBreakpointDp: Int): Boolean {
+ return minWidthDp >= widthBreakpointDp
}
/**
- * Returns `true` when [heightDp] is greater than or equal to [minHeightDp], `false` otherwise.
+ * Returns `true` when [minHeightDp] is greater than or equal to [heightBreakpointDp], `false`
+ * otherwise.
*/
- fun isHeightAtLeast(heightDp: Int): Boolean {
- return heightDp >= minHeightDp
+ fun isHeightAtLeastBreakpoint(heightBreakpointDp: Int): Boolean {
+ return minHeightDp >= heightBreakpointDp
}
/**
- * Returns `true` when [widthDp] is greater than or equal to [minWidthDp] and [heightDp] is
- * greater than or equal to [minHeightDp], `false` otherwise.
+ * Returns `true` when [widthBreakpointDp] is greater than or equal to [minWidthDp] and
+ * [heightBreakpointDp] is greater than or equal to [minHeightDp], `false` otherwise.
*/
- fun isAtLeast(widthDp: Int, heightDp: Int): Boolean {
- return isWidthAtLeast(widthDp) && isHeightAtLeast(heightDp)
+ fun isAtLeastBreakpoint(widthBreakpointDp: Int, heightBreakpointDp: Int): Boolean {
+ return isWidthAtLeastBreakpoint(widthBreakpointDp) &&
+ isHeightAtLeastBreakpoint(heightBreakpointDp)
}
override fun equals(other: Any?): Boolean {
diff --git a/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowSizeClassTest.kt b/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowSizeClassTest.kt
index 816e40c..e9a0aba 100644
--- a/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowSizeClassTest.kt
+++ b/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowSizeClassTest.kt
@@ -16,6 +16,10 @@
package androidx.window.core.layout
+import androidx.window.core.layout.WindowSizeClass.Companion.HEIGHT_DP_EXPANDED_LOWER_BOUND
+import androidx.window.core.layout.WindowSizeClass.Companion.HEIGHT_DP_MEDIUM_LOWER_BOUND
+import androidx.window.core.layout.WindowSizeClass.Companion.WIDTH_DP_EXPANDED_LOWER_BOUND
+import androidx.window.core.layout.WindowSizeClass.Companion.WIDTH_DP_MEDIUM_LOWER_BOUND
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
@@ -115,85 +119,252 @@
}
@Test
- fun is_width_at_least_returns_true_when_input_is_greater() {
+ fun is_width_at_least_breakpoint_returns_false_when_breakpoint_is_greater() {
val width = 200
val height = 100
val sizeClass = WindowSizeClass(width, height)
- assertTrue(sizeClass.isWidthAtLeast(width + 1))
+ assertFalse(sizeClass.isWidthAtLeastBreakpoint(width + 1))
}
@Test
- fun is_width_at_least_returns_true_when_input_is_equal() {
+ fun is_width_at_least_breakpoint_returns_true_when_breakpoint_is_equal() {
val width = 200
val height = 100
val sizeClass = WindowSizeClass(width, height)
- assertTrue(sizeClass.isWidthAtLeast(width))
+ assertTrue(sizeClass.isWidthAtLeastBreakpoint(width))
}
@Test
- fun is_width_at_least_returns_false_when_input_is_smaller() {
+ fun is_width_at_least_breakpoint_returns_true_when_breakpoint_is_smaller() {
val width = 200
val height = 100
val sizeClass = WindowSizeClass(width, height)
- assertFalse(sizeClass.isWidthAtLeast(width - 1))
+ assertTrue(sizeClass.isWidthAtLeastBreakpoint(width - 1))
+ }
+
+ /**
+ * Tests that the width breakpoint logic works as expected. The following sample shows what the
+ * dev use site should be
+ *
+ * WIDTH_DP_MEDIUM_LOWER_BOUND = 600 WIDTH_DP_EXPANDED_LOWER_BOUND = 840
+ *
+ * fun process(sizeClass: WindowSizeClass) { when {
+ * sizeClass.isWidthAtLeast(WIDTH_DP_EXPANDED_LOWER_BOUND) -> doExpanded()
+ * sizeClass.isWidthAtLeast(WIDTH_DP_MEDIUM_LOWER_BOUND) -> doMedium() else -> doCompact() } }
+ *
+ * val belowMediumBreakpoint = WindowSizeClass(minWidthDp = 300, minHeightDp = 0) val
+ * equalMediumBreakpoint = WindowSizeClass(minWidthDp = 600, minHeightDp = 0) val
+ * expandedBreakpoint = WindowSizeClass(minWidthDp = 840, minHeightDp = 0)
+ *
+ * process(belowBreakpoint) -> doSomethingCompact() process(equalMediumBreakpoint) ->
+ * doSomethingMedium() process(expandedBreakpoint) -> doSomethingExpanded()
+ *
+ * So the following must be true
+ *
+ * expandedBreakpoint WindowSizeClass(840, 0).isWidthAtLeast(WIDTH_DP_EXPANDED_LOWER_BOUND) ==
+ * true WindowSizeClass(840, 0).isWidthAtLeast(WIDTH_DP_MEDIUM_LOWER_BOUND) == true
+ *
+ * equalMediumBreakpoint WindowSizeClass(600, 0).isWidthAtLeast(WIDTH_DP_EXPANDED_LOWER_BOUND)
+ * == false WindowSizeClass(600, 0).isWidthAtLeast(WIDTH_DP_MEDIUM_LOWER_BOUND) == true
+ *
+ * belowBreakpoint WindowSizeClass(0, 0).isWidthAtLeast(WIDTH_DP_EXPANDED_LOWER_BOUND) == false
+ * WindowSizeClass(0, 0).isWidthAtLeast(WIDTH_DP_MEDIUM_LOWER_BOUND) == false
+ */
+ @Test
+ fun is_width_at_least_bounds_checks() {
+ // expandedBreakpoint
+ assertTrue(
+ WindowSizeClass(WIDTH_DP_EXPANDED_LOWER_BOUND, 0)
+ .isWidthAtLeastBreakpoint(WIDTH_DP_EXPANDED_LOWER_BOUND)
+ )
+ assertTrue(
+ WindowSizeClass(WIDTH_DP_EXPANDED_LOWER_BOUND, 0)
+ .isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND)
+ )
+
+ // equalMediumBreakpoint
+ assertFalse(
+ WindowSizeClass(WIDTH_DP_MEDIUM_LOWER_BOUND, 0)
+ .isWidthAtLeastBreakpoint(WIDTH_DP_EXPANDED_LOWER_BOUND)
+ )
+ assertTrue(
+ WindowSizeClass(WIDTH_DP_MEDIUM_LOWER_BOUND, 0)
+ .isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND)
+ )
+
+ // belowBreakpoint
+ assertFalse(WindowSizeClass(0, 0).isWidthAtLeastBreakpoint(WIDTH_DP_EXPANDED_LOWER_BOUND))
+ assertFalse(WindowSizeClass(0, 0).isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND))
+ }
+
+ /**
+ * Tests that the width breakpoint logic works as expected. The following sample shows what the
+ * dev use site should be
+ *
+ * HEIGHT_DP_MEDIUM_LOWER_BOUND = 480 HEIGHT_DP_EXPANDED_LOWER_BOUND = 900
+ *
+ * fun process(sizeClass: WindowSizeClass) { when {
+ * sizeClass.isHeightAtLeast(HEIGHT_DP_EXPANDED_LOWER_BOUND) -> doExpanded()
+ * sizeClass.isHeightAtLeast(HEIGHT_DP_MEDIUM_LOWER_BOUND) -> doMedium() else -> doCompact() } }
+ *
+ * val belowMediumBreakpoint = WindowSizeClass(minWidthDp = 0, minHeightDp = 0) val
+ * equalMediumBreakpoint = WindowSizeClass(minWidthDp = 0, minHeightDp = 480) val
+ * expandedBreakpoint = WindowSizeClass(minWidthDp = 0, minHeightDp = 900)
+ *
+ * process(belowBreakpoint) -> doSomethingCompact() process(equalMediumBreakpoint) ->
+ * doSomethingMedium() process(expandedBreakpoint) -> doSomethingExpanded()
+ *
+ * So the following must be true
+ *
+ * expandedBreakpoint WindowSizeClass(0, 900).isWidthAtLeast(HEIGHT_DP_EXPANDED_LOWER_BOUND) ==
+ * true WindowSizeClass(0, 900).isWidthAtLeast(HEIGHT_DP_MEDIUM_LOWER_BOUND) == true
+ *
+ * equalMediumBreakpoint WindowSizeClass(0, 480).isWidthAtLeast(HEIGHT_DP_EXPANDED_LOWER_BOUND)
+ * == false WindowSizeClass(0, 480).isWidthAtLeast(HEIGHT_DP_MEDIUM_LOWER_BOUND) == true
+ *
+ * belowBreakpoint WindowSizeClass(0, 0).isWidthAtLeast(HEIGHT_DP_EXPANDED_LOWER_BOUND) == false
+ * WindowSizeClass(0, 0).isWidthAtLeast(HEIGHT_DP_MEDIUM_LOWER_BOUND) == false
+ */
+ @Test
+ fun is_height_at_least_bounds_checks() {
+ // expandedBreakpoint
+ assertTrue(
+ WindowSizeClass(0, HEIGHT_DP_EXPANDED_LOWER_BOUND)
+ .isHeightAtLeastBreakpoint(HEIGHT_DP_EXPANDED_LOWER_BOUND)
+ )
+ assertTrue(
+ WindowSizeClass(0, HEIGHT_DP_EXPANDED_LOWER_BOUND)
+ .isHeightAtLeastBreakpoint(HEIGHT_DP_MEDIUM_LOWER_BOUND)
+ )
+
+ // equalMediumBreakpoint
+ assertFalse(
+ WindowSizeClass(0, HEIGHT_DP_MEDIUM_LOWER_BOUND)
+ .isHeightAtLeastBreakpoint(HEIGHT_DP_EXPANDED_LOWER_BOUND)
+ )
+ assertTrue(
+ WindowSizeClass(0, HEIGHT_DP_MEDIUM_LOWER_BOUND)
+ .isHeightAtLeastBreakpoint(HEIGHT_DP_MEDIUM_LOWER_BOUND)
+ )
+
+ // belowBreakpoint
+ assertFalse(WindowSizeClass(0, 0).isHeightAtLeastBreakpoint(HEIGHT_DP_EXPANDED_LOWER_BOUND))
+ assertFalse(WindowSizeClass(0, 0).isHeightAtLeastBreakpoint(HEIGHT_DP_MEDIUM_LOWER_BOUND))
+ }
+
+ /**
+ * Tests that the width breakpoint logic works as expected. The following sample shows what the
+ * dev use site should be
+ *
+ * DIAGONAL_BOUND_MEDIUM = 600, 600 DIAGONAL_BOUND_EXPANDED = 900, 900
+ *
+ * fun process(sizeClass: WindowSizeClass) { when { sizeClass.isAtLeast(DIAGONAL_BOUND_EXPANDED,
+ * DIAGONAL_BOUND_EXPANDED) -> doExpanded() sizeClass.isAtLeast(DIAGONAL_BOUND_MEDIUM,
+ * DIAGONAL_BOUND_MEDIUM) -> doMedium() else -> doCompact() } }
+ *
+ * val belowMediumBreakpoint = WindowSizeClass(minWidthDp = 0, minHeightDp = 0) val
+ * equalMediumBreakpoint = WindowSizeClass(minWidthDp = 600, minHeightDp = 600) val
+ * expandedBreakpoint = WindowSizeClass(minWidthDp = 900, minHeightDp = 900)
+ *
+ * process(belowBreakpoint) -> doSomethingCompact() process(equalMediumBreakpoint) ->
+ * doSomethingMedium() process(expandedBreakpoint) -> doSomethingExpanded()
+ *
+ * So the following must be true
+ *
+ * expandedBreakpoint WindowSizeClass(900, 900).isWidthAtLeast(WIDTH_DP_EXPANDED_LOWER_BOUND) ==
+ * true WindowSizeClass(900, 900).isWidthAtLeast(WIDTH_DP_MEDIUM_LOWER_BOUND) == true
+ *
+ * equalMediumBreakpoint WindowSizeClass(600, 600).isWidthAtLeast(WIDTH_DP_EXPANDED_LOWER_BOUND)
+ * == false WindowSizeClass(600, 600).isWidthAtLeast(WIDTH_DP_MEDIUM_LOWER_BOUND) == true
+ *
+ * belowBreakpoint WindowSizeClass(0, 0).isWidthAtLeast(WIDTH_DP_EXPANDED_LOWER_BOUND) == false
+ * WindowSizeClass(0, 0).isWidthAtLeast(WIDTH_DP_MEDIUM_LOWER_BOUND) == false
+ */
+ @Test
+ fun is_area_at_least_bounds_checks() {
+ val diagonalMedium = 600
+ val diagonalExpanded = 900
+ // expandedBreakpoint
+ assertTrue(
+ WindowSizeClass(diagonalExpanded, diagonalExpanded)
+ .isAtLeastBreakpoint(diagonalExpanded, diagonalExpanded)
+ )
+ assertTrue(
+ WindowSizeClass(diagonalExpanded, diagonalExpanded)
+ .isAtLeastBreakpoint(diagonalMedium, diagonalMedium)
+ )
+
+ // equalMediumBreakpoint
+ assertFalse(
+ WindowSizeClass(diagonalMedium, diagonalMedium)
+ .isAtLeastBreakpoint(diagonalExpanded, diagonalExpanded)
+ )
+ assertTrue(
+ WindowSizeClass(diagonalMedium, diagonalMedium)
+ .isAtLeastBreakpoint(diagonalMedium, diagonalMedium)
+ )
+
+ // belowBreakpoint
+ assertFalse(WindowSizeClass(0, 0).isAtLeastBreakpoint(diagonalExpanded, diagonalExpanded))
+ assertFalse(WindowSizeClass(0, 0).isAtLeastBreakpoint(diagonalMedium, diagonalMedium))
}
@Test
- fun is_height_at_least_returns_true_when_input_is_greater() {
+ fun is_height_at_least_breakpoint_returns_false_when_breakpoint_is_greater() {
val width = 200
val height = 100
val sizeClass = WindowSizeClass(width, height)
- assertTrue(sizeClass.isHeightAtLeast(height + 1))
+ assertFalse(sizeClass.isHeightAtLeastBreakpoint(height + 1))
}
@Test
- fun is_height_at_least_returns_true_when_input_is_equal() {
+ fun is_height_at_least_breakpoint_returns_true_when_breakpoint_is_equal() {
val width = 200
val height = 100
val sizeClass = WindowSizeClass(width, height)
- assertTrue(sizeClass.isHeightAtLeast(height))
+ assertTrue(sizeClass.isHeightAtLeastBreakpoint(height))
}
@Test
- fun is_height_at_least_returns_false_when_input_is_smaller() {
+ fun is_height_at_least_breakpoint_returns_true_when_breakpoint_is_smaller() {
val width = 200
val height = 100
val sizeClass = WindowSizeClass(width, height)
- assertFalse(sizeClass.isHeightAtLeast(height - 1))
+ assertTrue(sizeClass.isHeightAtLeastBreakpoint(height - 1))
}
@Test
- fun is_at_least_returns_true_when_input_is_greater() {
+ fun is_at_least_breakpoint_returns_false_when_breakpoint_is_greater() {
val width = 200
val height = 100
val sizeClass = WindowSizeClass(width, height)
- assertTrue(sizeClass.isAtLeast(width, height + 1))
- assertTrue(sizeClass.isAtLeast(width + 1, height))
+ assertFalse(sizeClass.isAtLeastBreakpoint(width, height + 1))
+ assertFalse(sizeClass.isAtLeastBreakpoint(width + 1, height))
}
@Test
- fun is_at_least_returns_true_when_input_is_equal() {
+ fun is_at_least_breakpoint_returns_true_when_breakpoint_is_equal() {
val width = 200
val height = 100
val sizeClass = WindowSizeClass(width, height)
- assertTrue(sizeClass.isAtLeast(width, height))
+ assertTrue(sizeClass.isAtLeastBreakpoint(width, height))
}
@Test
- fun is_at_least_returns_false_when_input_is_smaller() {
+ fun is_at_least_breakpoint_returns_true_when_breakpoint_is_smaller() {
val width = 200
val height = 100
val sizeClass = WindowSizeClass(width, height)
- assertFalse(sizeClass.isAtLeast(width, height - 1))
- assertFalse(sizeClass.isAtLeast(width - 1, height))
+ assertTrue(sizeClass.isAtLeastBreakpoint(width, height - 1))
+ assertTrue(sizeClass.isAtLeastBreakpoint(width - 1, height))
}
}
diff --git a/window/window-java/build.gradle b/window/window-java/build.gradle
index a64e31c..372d095 100644
--- a/window/window-java/build.gradle
+++ b/window/window-java/build.gradle
@@ -47,9 +47,9 @@
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- androidTestImplementation(libs.mockitoKotlin, excludes.bytebuddy)
+ androidTestImplementation(libs.dexmakerMockito)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.mockitoKotlin)
}
androidx {
diff --git a/window/window-rxjava2/build.gradle b/window/window-rxjava2/build.gradle
index 0a84a10..01a72dd 100644
--- a/window/window-rxjava2/build.gradle
+++ b/window/window-rxjava2/build.gradle
@@ -45,9 +45,9 @@
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- androidTestImplementation(libs.mockitoKotlin, excludes.bytebuddy)
+ androidTestImplementation(libs.dexmakerMockito)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.mockitoKotlin)
androidTestImplementation(libs.kotlinCoroutinesTest)
}
diff --git a/window/window-rxjava3/build.gradle b/window/window-rxjava3/build.gradle
index 27f2064..eece733 100644
--- a/window/window-rxjava3/build.gradle
+++ b/window/window-rxjava3/build.gradle
@@ -45,9 +45,9 @@
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- androidTestImplementation(libs.mockitoKotlin, excludes.bytebuddy)
+ androidTestImplementation(libs.dexmakerMockito)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.mockitoKotlin)
androidTestImplementation(libs.kotlinCoroutinesTest)
}
diff --git a/window/window/build.gradle b/window/window/build.gradle
index 004b04a..10c45a2 100644
--- a/window/window/build.gradle
+++ b/window/window/build.gradle
@@ -84,9 +84,9 @@
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- androidTestImplementation(libs.mockitoKotlin, excludes.bytebuddy)
+ androidTestImplementation(libs.dexmakerMockito)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.mockitoKotlin)
androidTestImplementation(libs.kotlinCoroutinesTest)
androidTestImplementation(libs.truth)
androidTestImplementation(libs.junit) // Needed for Assert.assertThrows
diff --git a/work/integration-tests/testapp/build.gradle b/work/integration-tests/testapp/build.gradle
index e9e5895..65f9532 100644
--- a/work/integration-tests/testapp/build.gradle
+++ b/work/integration-tests/testapp/build.gradle
@@ -44,9 +44,9 @@
}
dependencies {
- annotationProcessor(projectOrArtifact(":room:room-compiler"))
- implementation(projectOrArtifact(":room:room-runtime"))
- implementation(projectOrArtifact(":room:room-ktx"))
+ annotationProcessor(project(":room:room-compiler"))
+ implementation(project(":room:room-runtime"))
+ implementation(project(":room:room-ktx"))
implementation(libs.constraintLayout)
implementation("androidx.core:core:1.12.0")
diff --git a/work/work-benchmark/build.gradle b/work/work-benchmark/build.gradle
index 3ac1da2..c5e9353 100644
--- a/work/work-benchmark/build.gradle
+++ b/work/work-benchmark/build.gradle
@@ -34,8 +34,8 @@
androidTestImplementation(project(":work:work-runtime-ktx"))
androidTestImplementation(project(":work:work-multiprocess"))
androidTestImplementation(projectOrArtifact(":benchmark:benchmark-junit4"))
- androidTestImplementation(projectOrArtifact(":room:room-runtime"))
- androidTestImplementation(projectOrArtifact(":room:room-ktx"))
+ androidTestImplementation(project(":room:room-runtime"))
+ androidTestImplementation(project(":room:room-ktx"))
androidTestImplementation(libs.junit)
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
diff --git a/work/work-gcm/build.gradle b/work/work-gcm/build.gradle
index b316707..e06ae25 100644
--- a/work/work-gcm/build.gradle
+++ b/work/work-gcm/build.gradle
@@ -49,8 +49,8 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.espressoCore)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has its own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has its own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
testImplementation(libs.junit)
}
diff --git a/work/work-multiprocess/build.gradle b/work/work-multiprocess/build.gradle
index 692e72f..364b730 100644
--- a/work/work-multiprocess/build.gradle
+++ b/work/work-multiprocess/build.gradle
@@ -48,8 +48,8 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.espressoCore)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has its own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has its own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.truth)
}
diff --git a/work/work-runtime/build.gradle b/work/work-runtime/build.gradle
index 8a5ec39..94aa053 100644
--- a/work/work-runtime/build.gradle
+++ b/work/work-runtime/build.gradle
@@ -80,12 +80,12 @@
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
androidTestImplementation("androidx.arch.core:core-testing:2.2.0")
- androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-runtime-testing"))
+ androidTestImplementation(project(":lifecycle:lifecycle-runtime-testing"))
androidTestImplementation("androidx.room:room-testing:2.6.1")
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.espressoCore)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has its own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has its own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(project(":internal-testutils-runtime"))
testImplementation(libs.junit)
testImplementation(libs.truth)
diff --git a/work/work-testing/build.gradle b/work/work-testing/build.gradle
index a9caf52..a972b75 100644
--- a/work/work-testing/build.gradle
+++ b/work/work-testing/build.gradle
@@ -41,8 +41,8 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.espressoCore)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.truth)
testImplementation(libs.truth)