From 897f06344de212a4998b90da103696aec7c72041 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Fri, 19 Dec 2025 13:03:21 +0100 Subject: [PATCH 01/21] Migrate `material:material-navigation` to multiplatform Relnote: Migrate `material:material-navigation` to multiplatform Test: N/A Change-Id: I8828006ecb67a3c385e681554c556d2cbedb0ecd --- .../bcv/native/current.txt | 55 +++++++++++++++++++ .../material/material-navigation/build.gradle | 53 ++++++++++++------ .../navigation/BottomSheetNavigatorTest.kt | 0 .../navigation/NavGraphBuilderTest.kt | 0 .../navigation/SheetContentHostTest.kt | 0 .../internal/BackHandler.android.kt | 25 +++++++++ .../material/navigation/BottomSheet.kt | 0 .../navigation/BottomSheetNavigator.kt | 2 +- .../BottomSheetNavigatorDestinationBuilder.kt | 1 + .../material/navigation/NavGraphBuilder.kt | 1 + .../material/navigation/SheetContentHost.kt | 0 .../navigation/internal/BackHandler.kt | 21 +++++++ .../internal/BackHandler.nonAndroid.kt | 24 ++++++++ docs-tip-of-tree/build.gradle | 2 +- 14 files changed, 164 insertions(+), 20 deletions(-) create mode 100644 compose/material/material-navigation/bcv/native/current.txt rename compose/material/material-navigation/src/{androidTest/java => androidDeviceTest/kotlin}/androidx/compose/material/navigation/BottomSheetNavigatorTest.kt (100%) rename compose/material/material-navigation/src/{androidTest/java => androidDeviceTest/kotlin}/androidx/compose/material/navigation/NavGraphBuilderTest.kt (100%) rename compose/material/material-navigation/src/{androidTest/java => androidDeviceTest/kotlin}/androidx/compose/material/navigation/SheetContentHostTest.kt (100%) create mode 100644 compose/material/material-navigation/src/androidMain/kotlin/androidx/compose/material/navigation/internal/BackHandler.android.kt rename compose/material/material-navigation/src/{main/java => commonMain/kotlin}/androidx/compose/material/navigation/BottomSheet.kt (100%) rename compose/material/material-navigation/src/{main/java => commonMain/kotlin}/androidx/compose/material/navigation/BottomSheetNavigator.kt (99%) rename compose/material/material-navigation/src/{main/java => commonMain/kotlin}/androidx/compose/material/navigation/BottomSheetNavigatorDestinationBuilder.kt (98%) rename compose/material/material-navigation/src/{main/java => commonMain/kotlin}/androidx/compose/material/navigation/NavGraphBuilder.kt (99%) rename compose/material/material-navigation/src/{main/java => commonMain/kotlin}/androidx/compose/material/navigation/SheetContentHost.kt (100%) create mode 100644 compose/material/material-navigation/src/commonMain/kotlin/androidx/compose/material/navigation/internal/BackHandler.kt create mode 100644 compose/material/material-navigation/src/nonAndroidMain/kotlin/androidx/compose/material/navigation/internal/BackHandler.nonAndroid.kt diff --git a/compose/material/material-navigation/bcv/native/current.txt b/compose/material/material-navigation/bcv/native/current.txt new file mode 100644 index 0000000000000..b3d0260221810 --- /dev/null +++ b/compose/material/material-navigation/bcv/native/current.txt @@ -0,0 +1,55 @@ +// Klib ABI Dump +// Targets: [linuxX64.linuxx64Stubs] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +final class androidx.compose.material.navigation/BottomSheetNavigator : androidx.navigation/Navigator { // androidx.compose.material.navigation/BottomSheetNavigator|null[0] + constructor (androidx.compose.material/ModalBottomSheetState) // androidx.compose.material.navigation/BottomSheetNavigator.|(androidx.compose.material.ModalBottomSheetState){}[0] + + final val navigatorSheetState // androidx.compose.material.navigation/BottomSheetNavigator.navigatorSheetState|{}navigatorSheetState[0] + final fun (): androidx.compose.material.navigation/BottomSheetNavigatorSheetState // androidx.compose.material.navigation/BottomSheetNavigator.navigatorSheetState.|(){}[0] + + final fun createDestination(): androidx.compose.material.navigation/BottomSheetNavigator.Destination // androidx.compose.material.navigation/BottomSheetNavigator.createDestination|createDestination(){}[0] + final fun navigate(kotlin.collections/List, androidx.navigation/NavOptions?, androidx.navigation/Navigator.Extras?) // androidx.compose.material.navigation/BottomSheetNavigator.navigate|navigate(kotlin.collections.List;androidx.navigation.NavOptions?;androidx.navigation.Navigator.Extras?){}[0] + final fun onAttach(androidx.navigation/NavigatorState) // androidx.compose.material.navigation/BottomSheetNavigator.onAttach|onAttach(androidx.navigation.NavigatorState){}[0] + final fun popBackStack(androidx.navigation/NavBackStackEntry, kotlin/Boolean) // androidx.compose.material.navigation/BottomSheetNavigator.popBackStack|popBackStack(androidx.navigation.NavBackStackEntry;kotlin.Boolean){}[0] + + final class Destination : androidx.navigation/FloatingWindow, androidx.navigation/NavDestination { // androidx.compose.material.navigation/BottomSheetNavigator.Destination|null[0] + constructor (androidx.compose.material.navigation/BottomSheetNavigator, kotlin/Function4) // androidx.compose.material.navigation/BottomSheetNavigator.Destination.|(androidx.compose.material.navigation.BottomSheetNavigator;kotlin.Function4){}[0] + } +} + +final class androidx.compose.material.navigation/BottomSheetNavigatorDestinationBuilder : androidx.navigation/NavDestinationBuilder { // androidx.compose.material.navigation/BottomSheetNavigatorDestinationBuilder|null[0] + constructor (androidx.compose.material.navigation/BottomSheetNavigator, kotlin.reflect/KClass<*>, kotlin.collections/Map>, kotlin/Function4) // androidx.compose.material.navigation/BottomSheetNavigatorDestinationBuilder.|(androidx.compose.material.navigation.BottomSheetNavigator;kotlin.reflect.KClass<*>;kotlin.collections.Map>;kotlin.Function4){}[0] + constructor (androidx.compose.material.navigation/BottomSheetNavigator, kotlin/String, kotlin/Function4) // androidx.compose.material.navigation/BottomSheetNavigatorDestinationBuilder.|(androidx.compose.material.navigation.BottomSheetNavigator;kotlin.String;kotlin.Function4){}[0] +} + +final class androidx.compose.material.navigation/BottomSheetNavigatorSheetState { // androidx.compose.material.navigation/BottomSheetNavigatorSheetState|null[0] + constructor (androidx.compose.material/ModalBottomSheetState) // androidx.compose.material.navigation/BottomSheetNavigatorSheetState.|(androidx.compose.material.ModalBottomSheetState){}[0] + + final val currentValue // androidx.compose.material.navigation/BottomSheetNavigatorSheetState.currentValue|{}currentValue[0] + final fun (): androidx.compose.material/ModalBottomSheetValue // androidx.compose.material.navigation/BottomSheetNavigatorSheetState.currentValue.|(){}[0] + final val isVisible // androidx.compose.material.navigation/BottomSheetNavigatorSheetState.isVisible|{}isVisible[0] + final fun (): kotlin/Boolean // androidx.compose.material.navigation/BottomSheetNavigatorSheetState.isVisible.|(){}[0] + final val targetValue // androidx.compose.material.navigation/BottomSheetNavigatorSheetState.targetValue|{}targetValue[0] + final fun (): androidx.compose.material/ModalBottomSheetValue // androidx.compose.material.navigation/BottomSheetNavigatorSheetState.targetValue.|(){}[0] +} + +final val androidx.compose.material.navigation/androidx_compose_material_navigation_BottomSheetNavigator$stableprop // androidx.compose.material.navigation/androidx_compose_material_navigation_BottomSheetNavigator$stableprop|#static{}androidx_compose_material_navigation_BottomSheetNavigator$stableprop[0] +final val androidx.compose.material.navigation/androidx_compose_material_navigation_BottomSheetNavigatorDestinationBuilder$stableprop // androidx.compose.material.navigation/androidx_compose_material_navigation_BottomSheetNavigatorDestinationBuilder$stableprop|#static{}androidx_compose_material_navigation_BottomSheetNavigatorDestinationBuilder$stableprop[0] +final val androidx.compose.material.navigation/androidx_compose_material_navigation_BottomSheetNavigatorSheetState$stableprop // androidx.compose.material.navigation/androidx_compose_material_navigation_BottomSheetNavigatorSheetState$stableprop|#static{}androidx_compose_material_navigation_BottomSheetNavigatorSheetState$stableprop[0] +final val androidx.compose.material.navigation/androidx_compose_material_navigation_BottomSheetNavigator_Destination$stableprop // androidx.compose.material.navigation/androidx_compose_material_navigation_BottomSheetNavigator_Destination$stableprop|#static{}androidx_compose_material_navigation_BottomSheetNavigator_Destination$stableprop[0] + +final fun (androidx.navigation/NavGraphBuilder).androidx.compose.material.navigation/bottomSheet(kotlin.reflect/KClass<*>, kotlin.collections/Map>, kotlin.collections/List, kotlin.collections/List = ..., kotlin/Function4) // androidx.compose.material.navigation/bottomSheet|bottomSheet@androidx.navigation.NavGraphBuilder(kotlin.reflect.KClass<*>;kotlin.collections.Map>;kotlin.collections.List;kotlin.collections.List;kotlin.Function4){}[0] +final fun (androidx.navigation/NavGraphBuilder).androidx.compose.material.navigation/bottomSheet(kotlin/String, kotlin.collections/List = ..., kotlin.collections/List = ..., kotlin/Function4) // androidx.compose.material.navigation/bottomSheet|bottomSheet@androidx.navigation.NavGraphBuilder(kotlin.String;kotlin.collections.List;kotlin.collections.List;kotlin.Function4){}[0] +final fun androidx.compose.material.navigation/ModalBottomSheetLayout(androidx.compose.material.navigation/BottomSheetNavigator, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Shape?, androidx.compose.ui.unit/Dp, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, kotlin/Function2, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.material.navigation/ModalBottomSheetLayout|ModalBottomSheetLayout(androidx.compose.material.navigation.BottomSheetNavigator;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Shape?;androidx.compose.ui.unit.Dp;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;kotlin.Function2;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] +final fun androidx.compose.material.navigation/ModalBottomSheetLayout(androidx.compose.material.navigation/BottomSheetNavigator, androidx.compose.ui/Modifier?, kotlin/Boolean, androidx.compose.ui.graphics/Shape?, androidx.compose.ui.unit/Dp, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, kotlin/Function2, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.material.navigation/ModalBottomSheetLayout|ModalBottomSheetLayout(androidx.compose.material.navigation.BottomSheetNavigator;androidx.compose.ui.Modifier?;kotlin.Boolean;androidx.compose.ui.graphics.Shape?;androidx.compose.ui.unit.Dp;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;kotlin.Function2;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] +final fun androidx.compose.material.navigation/androidx_compose_material_navigation_BottomSheetNavigator$stableprop_getter(): kotlin/Int // androidx.compose.material.navigation/androidx_compose_material_navigation_BottomSheetNavigator$stableprop_getter|androidx_compose_material_navigation_BottomSheetNavigator$stableprop_getter(){}[0] +final fun androidx.compose.material.navigation/androidx_compose_material_navigation_BottomSheetNavigatorDestinationBuilder$stableprop_getter(): kotlin/Int // androidx.compose.material.navigation/androidx_compose_material_navigation_BottomSheetNavigatorDestinationBuilder$stableprop_getter|androidx_compose_material_navigation_BottomSheetNavigatorDestinationBuilder$stableprop_getter(){}[0] +final fun androidx.compose.material.navigation/androidx_compose_material_navigation_BottomSheetNavigatorSheetState$stableprop_getter(): kotlin/Int // androidx.compose.material.navigation/androidx_compose_material_navigation_BottomSheetNavigatorSheetState$stableprop_getter|androidx_compose_material_navigation_BottomSheetNavigatorSheetState$stableprop_getter(){}[0] +final fun androidx.compose.material.navigation/androidx_compose_material_navigation_BottomSheetNavigator_Destination$stableprop_getter(): kotlin/Int // androidx.compose.material.navigation/androidx_compose_material_navigation_BottomSheetNavigator_Destination$stableprop_getter|androidx_compose_material_navigation_BottomSheetNavigator_Destination$stableprop_getter(){}[0] +final fun androidx.compose.material.navigation/rememberBottomSheetNavigator(androidx.compose.animation.core/AnimationSpec?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.material.navigation/BottomSheetNavigator // androidx.compose.material.navigation/rememberBottomSheetNavigator|rememberBottomSheetNavigator(androidx.compose.animation.core.AnimationSpec?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] +final inline fun <#A: reified kotlin/Any> (androidx.navigation/NavGraphBuilder).androidx.compose.material.navigation/bottomSheet(kotlin.collections/Map> = ..., kotlin.collections/List = ..., kotlin.collections/List = ..., noinline kotlin/Function4) // androidx.compose.material.navigation/bottomSheet|bottomSheet@androidx.navigation.NavGraphBuilder(kotlin.collections.Map>;kotlin.collections.List;kotlin.collections.List;kotlin.Function4){0ยง}[0] diff --git a/compose/material/material-navigation/build.gradle b/compose/material/material-navigation/build.gradle index ae8618c4eb111..2f63b02960fc5 100644 --- a/compose/material/material-navigation/build.gradle +++ b/compose/material/material-navigation/build.gradle @@ -14,27 +14,49 @@ * limitations under the License. */ + +import androidx.build.PlatformIdentifier import androidx.build.SoftwareType plugins { id("AndroidXPlugin") - id("com.android.library") id("AndroidXComposePlugin") alias(libs.plugins.kotlinSerialization) } -dependencies { - api("androidx.navigation:navigation-compose:2.8.0") - implementation(project(":compose:material:material")) - implementation(libs.kotlinSerializationCore) - - androidTestImplementation(project(":compose:test-utils")) - androidTestImplementation("androidx.navigation:navigation-testing:2.7.7") - androidTestImplementation(project(":compose:ui:ui-test-junit4")) - androidTestImplementation(libs.testRunner) - androidTestImplementation(libs.junit) - androidTestImplementation(libs.truth) - androidTestImplementation(libs.testRules) +androidXMultiplatform { + androidLibrary { + namespace = "androidx.compose.material.navigation" + compileSdk { version = release(35) } + } + jvmStubs() + linuxX64Stubs() + + defaultPlatform(PlatformIdentifier.ANDROID) + + sourceSets { + commonMain.dependencies { + api("androidx.navigation:navigation-compose:2.9.6") + implementation(project(":compose:material:material")) + implementation(libs.kotlinSerializationCore) + } + + androidDeviceTest.dependencies { + implementation(project(":compose:test-utils")) + implementation("androidx.navigation:navigation-testing:2.9.6") + implementation(project(":compose:ui:ui-test-junit4")) + implementation(libs.testRunner) + implementation(libs.junit) + implementation(libs.truth) + implementation(libs.testRules) + } + nonAndroidMain.dependsOn(commonMain) + nonAndroidTest.dependsOn(commonTest) + jvmStubsMain.dependsOn(nonAndroidMain) + jvmStubsTest.dependsOn(nonAndroidTest) + nonJvmMain.dependsOn(nonAndroidMain) + nonJvmTest.dependsOn(nonAndroidTest) + } } androidx { @@ -46,8 +68,3 @@ androidx { legacyDisableKotlinStrictApiMode = true samples(project(":compose:material:material-navigation-samples")) } - -android { - compileSdk { version = release(35) } - namespace = "androidx.compose.material.navigation" -} diff --git a/compose/material/material-navigation/src/androidTest/java/androidx/compose/material/navigation/BottomSheetNavigatorTest.kt b/compose/material/material-navigation/src/androidDeviceTest/kotlin/androidx/compose/material/navigation/BottomSheetNavigatorTest.kt similarity index 100% rename from compose/material/material-navigation/src/androidTest/java/androidx/compose/material/navigation/BottomSheetNavigatorTest.kt rename to compose/material/material-navigation/src/androidDeviceTest/kotlin/androidx/compose/material/navigation/BottomSheetNavigatorTest.kt diff --git a/compose/material/material-navigation/src/androidTest/java/androidx/compose/material/navigation/NavGraphBuilderTest.kt b/compose/material/material-navigation/src/androidDeviceTest/kotlin/androidx/compose/material/navigation/NavGraphBuilderTest.kt similarity index 100% rename from compose/material/material-navigation/src/androidTest/java/androidx/compose/material/navigation/NavGraphBuilderTest.kt rename to compose/material/material-navigation/src/androidDeviceTest/kotlin/androidx/compose/material/navigation/NavGraphBuilderTest.kt diff --git a/compose/material/material-navigation/src/androidTest/java/androidx/compose/material/navigation/SheetContentHostTest.kt b/compose/material/material-navigation/src/androidDeviceTest/kotlin/androidx/compose/material/navigation/SheetContentHostTest.kt similarity index 100% rename from compose/material/material-navigation/src/androidTest/java/androidx/compose/material/navigation/SheetContentHostTest.kt rename to compose/material/material-navigation/src/androidDeviceTest/kotlin/androidx/compose/material/navigation/SheetContentHostTest.kt diff --git a/compose/material/material-navigation/src/androidMain/kotlin/androidx/compose/material/navigation/internal/BackHandler.android.kt b/compose/material/material-navigation/src/androidMain/kotlin/androidx/compose/material/navigation/internal/BackHandler.android.kt new file mode 100644 index 0000000000000..142ff7bf4280a --- /dev/null +++ b/compose/material/material-navigation/src/androidMain/kotlin/androidx/compose/material/navigation/internal/BackHandler.android.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2025 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.material.navigation.internal + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable + +@Composable +internal actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) { + BackHandler(enabled, onBack) +} diff --git a/compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/BottomSheet.kt b/compose/material/material-navigation/src/commonMain/kotlin/androidx/compose/material/navigation/BottomSheet.kt similarity index 100% rename from compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/BottomSheet.kt rename to compose/material/material-navigation/src/commonMain/kotlin/androidx/compose/material/navigation/BottomSheet.kt diff --git a/compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/BottomSheetNavigator.kt b/compose/material/material-navigation/src/commonMain/kotlin/androidx/compose/material/navigation/BottomSheetNavigator.kt similarity index 99% rename from compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/BottomSheetNavigator.kt rename to compose/material/material-navigation/src/commonMain/kotlin/androidx/compose/material/navigation/BottomSheetNavigator.kt index b5490f0ca60ab..ec2f21aa06999 100644 --- a/compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/BottomSheetNavigator.kt +++ b/compose/material/material-navigation/src/commonMain/kotlin/androidx/compose/material/navigation/BottomSheetNavigator.kt @@ -16,12 +16,12 @@ package androidx.compose.material.navigation -import androidx.activity.compose.BackHandler import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.SpringSpec import androidx.compose.foundation.layout.ColumnScope import androidx.compose.material.ModalBottomSheetState import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.material.navigation.internal.BackHandler import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect diff --git a/compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/BottomSheetNavigatorDestinationBuilder.kt b/compose/material/material-navigation/src/commonMain/kotlin/androidx/compose/material/navigation/BottomSheetNavigatorDestinationBuilder.kt similarity index 98% rename from compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/BottomSheetNavigatorDestinationBuilder.kt rename to compose/material/material-navigation/src/commonMain/kotlin/androidx/compose/material/navigation/BottomSheetNavigatorDestinationBuilder.kt index d2256c4c5897f..4c5ed31e3a686 100644 --- a/compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/BottomSheetNavigatorDestinationBuilder.kt +++ b/compose/material/material-navigation/src/commonMain/kotlin/androidx/compose/material/navigation/BottomSheetNavigatorDestinationBuilder.kt @@ -22,6 +22,7 @@ import androidx.navigation.NavBackStackEntry import androidx.navigation.NavDestinationBuilder import androidx.navigation.NavDestinationDsl import androidx.navigation.NavType +import kotlin.jvm.JvmSuppressWildcards import kotlin.reflect.KClass import kotlin.reflect.KType diff --git a/compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/NavGraphBuilder.kt b/compose/material/material-navigation/src/commonMain/kotlin/androidx/compose/material/navigation/NavGraphBuilder.kt similarity index 99% rename from compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/NavGraphBuilder.kt rename to compose/material/material-navigation/src/commonMain/kotlin/androidx/compose/material/navigation/NavGraphBuilder.kt index 868ae2f13b814..a8faed0e1b033 100644 --- a/compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/NavGraphBuilder.kt +++ b/compose/material/material-navigation/src/commonMain/kotlin/androidx/compose/material/navigation/NavGraphBuilder.kt @@ -25,6 +25,7 @@ import androidx.navigation.NavDeepLink import androidx.navigation.NavGraphBuilder import androidx.navigation.NavType import androidx.navigation.get +import kotlin.jvm.JvmSuppressWildcards import kotlin.reflect.KClass import kotlin.reflect.KType diff --git a/compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/SheetContentHost.kt b/compose/material/material-navigation/src/commonMain/kotlin/androidx/compose/material/navigation/SheetContentHost.kt similarity index 100% rename from compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/SheetContentHost.kt rename to compose/material/material-navigation/src/commonMain/kotlin/androidx/compose/material/navigation/SheetContentHost.kt diff --git a/compose/material/material-navigation/src/commonMain/kotlin/androidx/compose/material/navigation/internal/BackHandler.kt b/compose/material/material-navigation/src/commonMain/kotlin/androidx/compose/material/navigation/internal/BackHandler.kt new file mode 100644 index 0000000000000..5e6490e2320d0 --- /dev/null +++ b/compose/material/material-navigation/src/commonMain/kotlin/androidx/compose/material/navigation/internal/BackHandler.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2025 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.material.navigation.internal + +import androidx.compose.runtime.Composable + +@Composable internal expect fun BackHandler(enabled: Boolean = true, onBack: () -> Unit) diff --git a/compose/material/material-navigation/src/nonAndroidMain/kotlin/androidx/compose/material/navigation/internal/BackHandler.nonAndroid.kt b/compose/material/material-navigation/src/nonAndroidMain/kotlin/androidx/compose/material/navigation/internal/BackHandler.nonAndroid.kt new file mode 100644 index 0000000000000..7bba595270083 --- /dev/null +++ b/compose/material/material-navigation/src/nonAndroidMain/kotlin/androidx/compose/material/navigation/internal/BackHandler.nonAndroid.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2025 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.material.navigation.internal + +import androidx.navigation.compose.internal.implementedInJetBrainsFork + +@androidx.compose.runtime.Composable +internal actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) { + implementedInJetBrainsFork() +} diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle index b66ffac86a65b..5af8e8ce6fa7c 100644 --- a/docs-tip-of-tree/build.gradle +++ b/docs-tip-of-tree/build.gradle @@ -82,7 +82,7 @@ dependencies { kmpDocs(project(":compose:material3:xr:xr-adaptive")) kmpDocs(project(":compose:material:material")) kmpDocs(project(":compose:material:material-ripple")) - docs(project(":compose:material:material-navigation")) + kmpDocs(project(":compose:material:material-navigation")) docs(project(":compose:remote:remote-core")) kmpDocs(project(":compose:remote:remote-creation")) docs(project(":compose:remote:remote-creation-compose")) From 8b395c0caacb6930bd57e3f14347bc695131a46a Mon Sep 17 00:00:00 2001 From: Louis Pullen-Freilich Date: Wed, 28 Jan 2026 14:28:33 +0000 Subject: [PATCH 02/21] Rewrite Modifier.combinedClickable() without SuspendingPointerInputModifierNode We previously migrated clickable in this same manner (http://r.android.com/3650957), this CL applies this change to combinedClickable. performPointerInputClick_combinedClickable_withNoLongClickDefined Before: timeNs min 376,710.7, median 383,760.8, max 448,745.2 allocationCount min 161.3, median 161.3, max 161.3 After: timeNs min 239,602.3, median 243,738.7, max 304,694.3 allocationCount min 111.0, median 111.0, max 111.0 performPointerInputClick_combinedClickable_withLongClickDefined Before: timeNs min 516,184.5, median 527,394.5, max 623,607.9 allocationCount min 203.5, median 203.5, max 203.5 After: timeNs min 412,102.7, median 432,991.3, max 578,558.9 allocationCount min 156.2, median 156.3, max 156.3 performPointerInputDoubleClick_combinedClickable_withLongClickDefined Before: timeNs min 1,171,422.2, median 1,187,610.0, max 1,408,080.7 allocationCount min 436.2, median 436.3, max 436.3 After: timeNs min 924,680.5, median 953,959.2, max 1,189,148.9 allocationCount min 353.8, median 353.8, max 353.8 Bug: b/477836055 Test: CombinedClickableBenchmark Test: CombinedClickableTest Relnote: "Modifier.combinedClickable is rewritten to not use suspend pointer input as an optimization. This feature is enabled by the flag you can disable if encounter a bug in the new implementation - ComposeFoundationFlags.isNonSuspendingPointerInputInCombinedClickableEnabled." Change-Id: Iea684f0973b8407920f452b94795ae913ec05f28 --- compose/foundation/foundation/api/current.txt | 2 + .../foundation/api/restricted_current.txt | 2 + .../benchmark/ClickableBenchmark.kt | 53 ++- .../benchmark/CombinedClickableBenchmark.kt | 200 ++++++++ .../compose/foundation/ClickableTest.kt | 31 -- .../foundation/CombinedClickableTest.kt | 431 +++++++++++++++++- .../androidx/compose/foundation/Clickable.kt | 255 +++++++++-- .../foundation/ComposeFoundationFlags.kt | 8 + 8 files changed, 891 insertions(+), 91 deletions(-) create mode 100644 compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/CombinedClickableBenchmark.kt diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt index b1ec173e77095..1e662a29eebdb 100644 --- a/compose/foundation/foundation/api/current.txt +++ b/compose/foundation/foundation/api/current.txt @@ -156,6 +156,7 @@ package androidx.compose.foundation { property public boolean isDelayPressesUsingGestureConsumptionEnabled; property public boolean isNestedDraggablesTouchConflictFixEnabled; property public boolean isNewContextMenuEnabled; + property public boolean isNonSuspendingPointerInputInCombinedClickableEnabled; property public boolean isPausableCompositionInPrefetchEnabled; property public boolean isSmartSelectionEnabled; property public boolean isTrackpadGestureHandlingEnabled; @@ -167,6 +168,7 @@ package androidx.compose.foundation { field public static boolean isDelayPressesUsingGestureConsumptionEnabled; field public static boolean isNestedDraggablesTouchConflictFixEnabled; field public static boolean isNewContextMenuEnabled; + field public static boolean isNonSuspendingPointerInputInCombinedClickableEnabled; field public static boolean isPausableCompositionInPrefetchEnabled; field public static boolean isSmartSelectionEnabled; field public static boolean isTrackpadGestureHandlingEnabled; diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt index de1e4676636a5..3d2796b144f20 100644 --- a/compose/foundation/foundation/api/restricted_current.txt +++ b/compose/foundation/foundation/api/restricted_current.txt @@ -156,6 +156,7 @@ package androidx.compose.foundation { property public boolean isDelayPressesUsingGestureConsumptionEnabled; property public boolean isNestedDraggablesTouchConflictFixEnabled; property public boolean isNewContextMenuEnabled; + property public boolean isNonSuspendingPointerInputInCombinedClickableEnabled; property public boolean isPausableCompositionInPrefetchEnabled; property public boolean isSmartSelectionEnabled; property public boolean isTrackpadGestureHandlingEnabled; @@ -167,6 +168,7 @@ package androidx.compose.foundation { field public static boolean isDelayPressesUsingGestureConsumptionEnabled; field public static boolean isNestedDraggablesTouchConflictFixEnabled; field public static boolean isNewContextMenuEnabled; + field public static boolean isNonSuspendingPointerInputInCombinedClickableEnabled; field public static boolean isPausableCompositionInPrefetchEnabled; field public static boolean isSmartSelectionEnabled; field public static boolean isTrackpadGestureHandlingEnabled; diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/ClickableBenchmark.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/ClickableBenchmark.kt index 8f373304e0b41..b59b4ef6671d3 100644 --- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/ClickableBenchmark.kt +++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/ClickableBenchmark.kt @@ -82,7 +82,7 @@ private class ClickablePerformClickTestCase : ComposeTestCase { var isPressed = false private set - private val indication = EmptyIndication() + private val indication = EmptyIndication { isPressed = it } @Composable override fun Content() { @@ -90,33 +90,44 @@ private class ClickablePerformClickTestCase : ComposeTestCase { Box(Modifier.fillMaxSize().clickable(onClick = { clickCount++ })) } } +} + +internal class EmptyIndication(private val setIsPressed: (Boolean) -> Unit) : + IndicationNodeFactory { - private inner class EmptyIndication : IndicationNodeFactory { + override fun create(interactionSource: InteractionSource): DelegatableNode = + EmptyIndicationInstance(interactionSource, setIsPressed) - override fun create(interactionSource: InteractionSource): DelegatableNode = - EmptyIndicationInstance(interactionSource) + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is EmptyIndication) return false - override fun hashCode(): Int = -1 + if (setIsPressed !== other.setIsPressed) return false - override fun equals(other: Any?) = other === this + return true + } - private inner class EmptyIndicationInstance( - private val interactionSource: InteractionSource - ) : Modifier.Node() { + override fun hashCode(): Int { + return setIsPressed.hashCode() + } - // it is a simplified version of what is happening in the real indications like ripple. - // as delivering interactions updates is a crucial part of what is happening during - // click, we should have it as part of this benchmark. - override fun onAttach() { - coroutineScope.launch { - var pressCount = 0 - interactionSource.interactions.collect { interaction -> - when (interaction) { - is PressInteraction.Press -> pressCount++ - is PressInteraction.Release -> pressCount-- - } - isPressed = pressCount > 0 + private class EmptyIndicationInstance( + private val interactionSource: InteractionSource, + private val setIsPressed: (Boolean) -> Unit, + ) : Modifier.Node() { + + // it is a simplified version of what is happening in the real indications like ripple. + // as delivering interactions updates is a crucial part of what is happening during + // click, we should have it as part of this benchmark. + override fun onAttach() { + coroutineScope.launch { + var pressCount = 0 + interactionSource.interactions.collect { interaction -> + when (interaction) { + is PressInteraction.Press -> pressCount++ + is PressInteraction.Release -> pressCount-- } + setIsPressed(pressCount > 0) } } } diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/CombinedClickableBenchmark.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/CombinedClickableBenchmark.kt new file mode 100644 index 0000000000000..e5a0f4c3b9e85 --- /dev/null +++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/CombinedClickableBenchmark.kt @@ -0,0 +1,200 @@ +/* + * Copyright 2026 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.benchmark + +import android.view.MotionEvent +import android.view.View +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.benchmark.lazy.MotionEventHelper +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.testutils.ComposeTestCase +import androidx.compose.testutils.benchmark.ComposeBenchmarkRule +import androidx.compose.testutils.doFramesUntilNoChangesPending +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.platform.LocalViewConfiguration +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalFoundationApi::class) +@LargeTest +@RunWith(AndroidJUnit4::class) +class CombinedClickableBenchmark { + @get:Rule val benchmarkRule = ComposeBenchmarkRule() + + @Test + fun performPointerInputClick_combinedClickable_withNoLongClickDefined() { + benchmarkRule.runBenchmarkFor({ + CombinedClickablePerformClickTestCase(longClickDefined = false) + }) { + lateinit var case: CombinedClickablePerformClickTestCase + lateinit var rootView: View + + benchmarkRule.runOnUiThread { + doFramesUntilNoChangesPending() + case = getTestCase() + rootView = getHostView() + } + + val motionEventHelper = MotionEventHelper(rootView) + var expectedClickCount = case.clickCount + benchmarkRule.measureRepeatedOnUiThread { + assertThat(case.isPressed).isFalse() + val viewCenter = Offset(rootView.measuredWidth / 2f, rootView.measuredHeight / 2f) + motionEventHelper.sendEvent(MotionEvent.ACTION_DOWN, viewCenter) + assertThat(case.isPressed).isTrue() + motionEventHelper.sendEvent(MotionEvent.ACTION_UP, Offset.Zero) + assertThat(case.isPressed).isFalse() + expectedClickCount++ + assertThat(case.clickCount).isEqualTo(expectedClickCount) + } + } + } + + @Test + fun performPointerInputClick_combinedClickable_withLongClickDefined() { + benchmarkRule.runBenchmarkFor({ + CombinedClickablePerformClickTestCase(longClickDefined = true) + }) { + lateinit var case: CombinedClickablePerformClickTestCase + lateinit var rootView: View + + benchmarkRule.runOnUiThread { + doFramesUntilNoChangesPending() + case = getTestCase() + rootView = getHostView() + } + + val motionEventHelper = MotionEventHelper(rootView) + var expectedClickCount = case.clickCount + benchmarkRule.measureRepeatedOnUiThread { + assertThat(case.isPressed).isFalse() + val viewCenter = Offset(rootView.measuredWidth / 2f, rootView.measuredHeight / 2f) + motionEventHelper.sendEvent(MotionEvent.ACTION_DOWN, viewCenter) + assertThat(case.isPressed).isTrue() + motionEventHelper.sendEvent(MotionEvent.ACTION_UP, Offset.Zero) + assertThat(case.isPressed).isFalse() + expectedClickCount++ + assertThat(case.clickCount).isEqualTo(expectedClickCount) + } + } + } + + @Test + fun performPointerInputDoubleClick_combinedClickable_withLongClickDefined() { + benchmarkRule.runBenchmarkFor({ CombinedClickablePerformDoubleClickTestCase() }) { + lateinit var case: CombinedClickablePerformDoubleClickTestCase + lateinit var rootView: View + + benchmarkRule.runOnUiThread { + doFramesUntilNoChangesPending() + case = getTestCase() + rootView = getHostView() + } + + val motionEventHelper = MotionEventHelper(rootView) + var expectedDoubleClickCount = case.doubleClickCount + val doubleTapMinTimeMillis = case.doubleTapMinTimeMillis + benchmarkRule.measureRepeatedOnUiThread { + assertThat(case.isPressed).isFalse() + val viewCenter = Offset(rootView.measuredWidth / 2f, rootView.measuredHeight / 2f) + motionEventHelper.sendEvent(MotionEvent.ACTION_DOWN, viewCenter) + assertThat(case.isPressed).isTrue() + motionEventHelper.sendEvent(MotionEvent.ACTION_UP, Offset.Zero) + assertThat(case.isPressed).isFalse() + motionEventHelper.sendEvent( + MotionEvent.ACTION_DOWN, + viewCenter, + timeDelta = doubleTapMinTimeMillis!! * 2, + ) + assertThat(case.isPressed).isTrue() + motionEventHelper.sendEvent(MotionEvent.ACTION_UP, Offset.Zero) + assertThat(case.isPressed).isFalse() + expectedDoubleClickCount++ + assertThat(case.doubleClickCount).isEqualTo(expectedDoubleClickCount) + assertThat(case.clickCount).isEqualTo(0) + } + } + } +} + +private class CombinedClickablePerformClickTestCase(private val longClickDefined: Boolean) : + ComposeTestCase { + var clickCount = 0 + private set + + var isPressed = false + private set + + private val indication = EmptyIndication { isPressed = it } + + @Composable + override fun Content() { + CompositionLocalProvider(LocalIndication provides indication) { + Box( + Modifier.fillMaxSize() + .combinedClickable( + onClick = { clickCount++ }, + onLongClick = + if (longClickDefined) { + {} + } else null, + ) + ) + } + } +} + +private class CombinedClickablePerformDoubleClickTestCase : ComposeTestCase { + var clickCount = 0 + private set + + var doubleClickCount = 0 + private set + + var isPressed = false + private set + + var doubleTapMinTimeMillis: Long? = null + private set + + private val indication = EmptyIndication { isPressed = it } + + @Composable + override fun Content() { + CompositionLocalProvider(LocalIndication provides indication) { + doubleTapMinTimeMillis = LocalViewConfiguration.current.doubleTapMinTimeMillis + Box( + Modifier.fillMaxSize() + .combinedClickable( + onClick = { clickCount++ }, + onDoubleClick = { doubleClickCount++ }, + onLongClick = {}, + ) + ) + } + } +} diff --git a/compose/foundation/foundation/src/androidDeviceTest/kotlin/androidx/compose/foundation/ClickableTest.kt b/compose/foundation/foundation/src/androidDeviceTest/kotlin/androidx/compose/foundation/ClickableTest.kt index 05554d2ecbba5..4c15e91711439 100644 --- a/compose/foundation/foundation/src/androidDeviceTest/kotlin/androidx/compose/foundation/ClickableTest.kt +++ b/compose/foundation/foundation/src/androidDeviceTest/kotlin/androidx/compose/foundation/ClickableTest.kt @@ -7377,37 +7377,6 @@ class ClickableTest { rule.onNodeWithTag(tag).assertIsFocused() } - @Test - fun lazilyCreatedIndicatorReceivesPressedInteraction() { - var created = false - val interactions = mutableListOf() - val indication = TestIndicationNodeFactory { interactionSource, coroutineScope -> - created = true - coroutineScope.launch { - interactionSource.interactions.collect { interaction -> - interactions.add(interaction) - } - } - } - - rule.setContent { - CompositionLocalProvider(LocalIndication provides indication) { - Box(modifier = Modifier.testTag("clickable").clickable {}) - } - } - - rule.runOnIdle { assertThat(created).isFalse() } - - // The touch event should cause the indication node to be created - rule.onNodeWithTag("clickable").performTouchInput { down(center) } - - rule.runOnIdle { - assertThat(created).isTrue() - assertThat(interactions).hasSize(1) - assertThat(interactions.first()).isInstanceOf(PressInteraction.Press::class.java) - } - } - /** * Regression test for b/414319919 - when inside a scrollable container (presses are delayed), * if a press, release, and press happen before coroutines are dispatched (in real life this is diff --git a/compose/foundation/foundation/src/androidDeviceTest/kotlin/androidx/compose/foundation/CombinedClickableTest.kt b/compose/foundation/foundation/src/androidDeviceTest/kotlin/androidx/compose/foundation/CombinedClickableTest.kt index 4578e84d51860..a91604acb2517 100644 --- a/compose/foundation/foundation/src/androidDeviceTest/kotlin/androidx/compose/foundation/CombinedClickableTest.kt +++ b/compose/foundation/foundation/src/androidDeviceTest/kotlin/androidx/compose/foundation/CombinedClickableTest.kt @@ -22,6 +22,7 @@ import android.view.InputDevice import android.view.MotionEvent import android.view.MotionEvent.ACTION_DOWN import android.view.MotionEvent.ACTION_MOVE +import android.view.MotionEvent.ACTION_UP import android.view.MotionEvent.CLASSIFICATION_DEEP_PRESS import android.view.MotionEvent.CLASSIFICATION_NONE import android.view.View @@ -67,6 +68,9 @@ import androidx.compose.ui.input.InputMode.Companion.Touch import androidx.compose.ui.input.InputModeManager import androidx.compose.ui.input.indirect.IndirectPointerEventPrimaryDirectionalMotionAxis import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.pointer.PointerEvent +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.InspectableValue import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalHapticFeedback @@ -101,6 +105,7 @@ import androidx.compose.ui.test.performSemanticsAction import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.pressKey import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastAll import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import androidx.test.filters.MediumTest @@ -586,6 +591,67 @@ class CombinedClickableTest { } } + @Test + @LargeTest + fun longClick_consumesEventsAfterLongClick() { + var counter = 0 + val onClick: () -> Unit = { ++counter } + val receivedEvents = mutableListOf() + + rule.setContent { + Box { + BasicText( + "ClickableText", + modifier = + Modifier.testTag("myClickable") + .pointerInput(Unit) { + awaitPointerEventScope { + while (true) { + val event = awaitPointerEvent() + receivedEvents += event + } + } + } + .combinedClickable(onLongClick = onClick) {}, + ) + } + } + + rule.onNodeWithTag("myClickable").performTouchInput { + down(center) + moveBy(Offset(1f, 1f)) + } + + rule.runOnIdle { + assertThat(counter).isEqualTo(0) + assertThat(receivedEvents.size).isEqualTo(2) + // Long click has not triggered yet, so the first move should not be consumed + assertThat(receivedEvents[0].type).isEqualTo(PointerEventType.Press) + assertThat(receivedEvents[0].changes.fastAll { it.isConsumed }).isTrue() + assertThat(receivedEvents[1].type).isEqualTo(PointerEventType.Move) + assertThat(receivedEvents[1].changes.fastAll { it.isConsumed }).isFalse() + receivedEvents.clear() + } + + rule.onNodeWithTag("myClickable").performTouchInput { + val longPressTimeout = viewConfiguration.longPressTimeoutMillis + 100 + advanceEventTime(longPressTimeout) + moveBy(Offset(1f, 1f)) + up() + } + + rule.runOnIdle { + // Long click will now have triggered + assertThat(counter).isEqualTo(1) + assertThat(receivedEvents.size).isEqualTo(2) + // Long click should consume the subsequent move and up + assertThat(receivedEvents[0].type).isEqualTo(PointerEventType.Move) + assertThat(receivedEvents[0].changes.fastAll { it.isConsumed }).isTrue() + assertThat(receivedEvents[1].type).isEqualTo(PointerEventType.Release) + assertThat(receivedEvents[1].changes.fastAll { it.isConsumed }).isTrue() + } + } + @Test fun click_withLongClick() { var clickCounter = 0 @@ -2277,6 +2343,71 @@ class CombinedClickableTest { } } + @Test + @LargeTest + fun longClick_interactionSource_cancelsIfBecomesDisabled() { + val interactionSource = MutableInteractionSource() + + var counter = 0 + var enabled by mutableStateOf(true) + + lateinit var scope: CoroutineScope + + rule.mainClock.autoAdvance = false + + rule.setContent { + scope = rememberCoroutineScope() + Box { + BasicText( + "ClickableText", + modifier = + Modifier.testTag("myClickable").combinedClickable( + enabled = enabled, + onLongClick = { counter++ }, + interactionSource = interactionSource, + indication = null, + ) {}, + ) + } + } + + val interactions = mutableListOf() + + scope.launch { interactionSource.interactions.collect { interactions.add(it) } } + + rule.runOnIdle { + assertThat(interactions).isEmpty() + assertThat(counter).isEqualTo(0) + } + + rule.onNodeWithTag("myClickable").performTouchInput { down(center) } + + // Initial press + rule.mainClock.advanceTimeBy(100) + + rule.runOnIdle { + assertThat(interactions).hasSize(1) + assertThat(interactions.first()).isInstanceOf(PressInteraction.Press::class.java) + assertThat(counter).isEqualTo(0) + } + + // Long click + rule.mainClock.advanceTimeBy(1000) + + rule.runOnIdle { enabled = false } + rule.mainClock.advanceTimeByFrame() + + // We should now be disabled, and so we should cancel the existing press. + rule.runOnIdle { + assertThat(interactions).hasSize(2) + assertThat(interactions.first()).isInstanceOf(PressInteraction.Press::class.java) + assertThat(interactions[1]).isInstanceOf(PressInteraction.Cancel::class.java) + assertThat((interactions[1] as PressInteraction.Cancel).press) + .isEqualTo(interactions[0]) + assertThat(counter).isEqualTo(1) + } + } + @Test @LargeTest fun click_withDoubleClick_andLongClick_disabled() { @@ -2369,6 +2500,50 @@ class CombinedClickableTest { } } + @Test + @LargeTest + fun click_withDoubleClick_andLongClick_disabledMidGesture() { + val enabled = mutableStateOf(true) + var clickCounter = 0 + var doubleClickCounter = 0 + var longClickCounter = 0 + val onClick: () -> Unit = { ++clickCounter } + val onDoubleClick: () -> Unit = { ++doubleClickCounter } + val onLongClick: () -> Unit = { ++longClickCounter } + + rule.setContent { + Box { + BasicText( + "ClickableText", + modifier = + Modifier.testTag("myClickable") + .combinedClickable( + enabled = enabled.value, + onDoubleClick = onDoubleClick, + onLongClick = onLongClick, + onClick = onClick, + ), + ) + } + } + + rule.onNodeWithTag("myClickable").performTouchInput { down(center) } + + rule.runOnIdle { enabled.value = false } + + // Process gestures + rule.mainClock.advanceTimeBy(1000) + + rule.onNodeWithTag("myClickable").performTouchInput { up() } + + // No gestures should be triggered since we became disabled mid-gesture + rule.runOnIdle { + assertThat(doubleClickCounter).isEqualTo(0) + assertThat(longClickCounter).isEqualTo(0) + assertThat(clickCounter).isEqualTo(0) + } + } + @Test @LargeTest fun clicks_consumedWhenDisabled() { @@ -2955,11 +3130,38 @@ class CombinedClickableTest { /* classification = */ CLASSIFICATION_DEEP_PRESS, ) + val upEvent = + MotionEvent.obtain( + /* downTime = */ 0, + /* eventTime = */ 100, + /* action = */ ACTION_UP, + /* pointerCount = */ 1, + /* pointerProperties = */ pointerProperties, + /* pointerCoords = */ arrayOf( + MotionEvent.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_NONE, + ) + view.dispatchTouchEvent(deepPressMoveEvent) rule.mainClock.advanceTimeBy(50) + view.dispatchTouchEvent(upEvent) + rule.mainClock.advanceTimeBy(50) // Even though the timeout didn't pass, the deep press should immediately trigger the long - // click + // click. No other callbacks should be triggered. rule.runOnIdle { assertThat(clicks).isEqualTo(0) assertThat(longClicks).isEqualTo(1) @@ -2967,6 +3169,204 @@ class CombinedClickableTest { } } + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + @Test + fun longClick_consumesEventsAfterLongClick_deepPress() { + lateinit var view: View + var counter = 0 + val onClick: () -> Unit = { ++counter } + val receivedEvents = mutableListOf() + + rule.setContent { + view = LocalView.current + Box { + BasicText( + "ClickableText", + modifier = + Modifier.testTag("myClickable") + .pointerInput(Unit) { + awaitPointerEventScope { + while (true) { + val event = awaitPointerEvent() + receivedEvents += event + } + } + } + .combinedClickable(onLongClick = onClick) {}, + ) + } + } + + 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( + MotionEvent.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, + ) + + val moveEvent = + MotionEvent.obtain( + /* downTime = */ 0, + /* eventTime = */ 50, + /* action = */ ACTION_MOVE, + /* pointerCount = */ 1, + /* pointerProperties = */ pointerProperties, + /* pointerCoords = */ arrayOf( + MotionEvent.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_NONE, + ) + + view.dispatchTouchEvent(downEvent) + rule.mainClock.advanceTimeBy(50) + view.dispatchTouchEvent(moveEvent) + rule.mainClock.advanceTimeBy(50) + + rule.runOnIdle { + assertThat(counter).isEqualTo(0) + assertThat(receivedEvents.size).isEqualTo(2) + // Long click has not triggered yet, so the first move should not be consumed + assertThat(receivedEvents[0].type).isEqualTo(PointerEventType.Press) + assertThat(receivedEvents[0].changes.fastAll { it.isConsumed }).isTrue() + assertThat(receivedEvents[1].type).isEqualTo(PointerEventType.Move) + assertThat(receivedEvents[1].changes.fastAll { it.isConsumed }).isFalse() + receivedEvents.clear() + } + + val deepPressMoveEvent = + MotionEvent.obtain( + /* downTime = */ 0, + /* eventTime = */ 100, + /* action = */ ACTION_MOVE, + /* pointerCount = */ 1, + /* pointerProperties = */ pointerProperties, + /* pointerCoords = */ arrayOf( + MotionEvent.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_DEEP_PRESS, + ) + + val postDeepPressMoveEvent = + MotionEvent.obtain( + /* downTime = */ 0, + /* eventTime = */ 150, + /* action = */ ACTION_MOVE, + /* pointerCount = */ 1, + /* pointerProperties = */ pointerProperties, + /* pointerCoords = */ arrayOf( + MotionEvent.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_NONE, + ) + + val upEvent = + MotionEvent.obtain( + /* downTime = */ 0, + /* eventTime = */ 200, + /* action = */ ACTION_UP, + /* pointerCount = */ 1, + /* pointerProperties = */ pointerProperties, + /* pointerCoords = */ arrayOf( + MotionEvent.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_NONE, + ) + + view.dispatchTouchEvent(deepPressMoveEvent) + rule.mainClock.advanceTimeBy(50) + view.dispatchTouchEvent(postDeepPressMoveEvent) + rule.mainClock.advanceTimeBy(50) + view.dispatchTouchEvent(upEvent) + rule.mainClock.advanceTimeBy(50) + + rule.runOnIdle { + // Long click will now have triggered + assertThat(counter).isEqualTo(1) + assertThat(receivedEvents.size).isEqualTo(3) + // Both the deep press event and subsequent move and up should be consumed + assertThat(receivedEvents[0].type).isEqualTo(PointerEventType.Move) + assertThat(receivedEvents[0].changes.fastAll { it.isConsumed }).isTrue() + assertThat(receivedEvents[1].type).isEqualTo(PointerEventType.Move) + assertThat(receivedEvents[1].changes.fastAll { it.isConsumed }).isTrue() + assertThat(receivedEvents[2].type).isEqualTo(PointerEventType.Release) + assertThat(receivedEvents[2].changes.fastAll { it.isConsumed }).isTrue() + } + } + /** Detect the second deep press as long click. */ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @Test @@ -3073,11 +3473,38 @@ class CombinedClickableTest { /* classification = */ CLASSIFICATION_DEEP_PRESS, ) + val upEvent = + MotionEvent.obtain( + /* downTime = */ 0, + /* eventTime = */ 150, + /* action = */ ACTION_UP, + /* pointerCount = */ 1, + /* pointerProperties = */ pointerProperties, + /* pointerCoords = */ arrayOf( + MotionEvent.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_NONE, + ) + view.dispatchTouchEvent(deepPressMoveEvent) rule.mainClock.advanceTimeBy(50) + view.dispatchTouchEvent(upEvent) + rule.mainClock.advanceTimeBy(50) // Even though the timeout didn't pass, the deep press should immediately trigger the long - // press + // click. No other callbacks should be triggered. rule.runOnIdle { assertThat(clicks).isEqualTo(0) assertThat(longClicks).isEqualTo(1) diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt index ff915e6b87848..668c54a3c7b6e 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt @@ -20,8 +20,10 @@ import androidx.collection.mutableLongObjectMapOf import androidx.compose.foundation.ComposeFoundationFlags.isDelayPressesUsingGestureConsumptionEnabled import androidx.compose.foundation.gestures.PressGestureScope import androidx.compose.foundation.gestures.ScrollableContainerNode +import androidx.compose.foundation.gestures.changedToDownIgnoreConsumed import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.isChangedToDown +import androidx.compose.foundation.gestures.isDeepPress import androidx.compose.foundation.interaction.HoverInteraction import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction @@ -871,17 +873,6 @@ internal open class ClickableNode( onClick = onClick, ) { - private fun getExtendedTouchPadding(size: IntSize): Size { - // copied from SuspendingPointerInputModifierNodeImpl.extendedTouchPadding: - // TODO expose this as a new public api available outside of suspending apis b/422396609 - val minimumTouchTargetSizeDp = currentValueOf(LocalViewConfiguration).minimumTouchTargetSize - val minimumTouchTargetSize = with(requireDensity()) { minimumTouchTargetSizeDp.toSize() } - val size = size - val horizontal = max(0f, minimumTouchTargetSize.width - size.width) / 2f - val vertical = max(0f, minimumTouchTargetSize.height - size.height) / 2f - return Size(horizontal, vertical) - } - private var downEvent: PointerInputChange? = null @OptIn(ExperimentalFoundationApi::class) @@ -956,7 +947,7 @@ internal open class ClickableNode( onClick: () -> Unit, ) { // enabled and onClick are captured inside callbacks, not as an input to detectTapGestures, - // so no need need to reset pointer input handling when they change + // so no need to reset pointer input handling when they change updateCommon( interactionSource = interactionSource, indicationNodeFactory = indicationNodeFactory, @@ -1005,34 +996,213 @@ private class CombinedClickableNode( private val longKeyPressJobs = mutableLongObjectMapOf() private val doubleKeyClickStates = mutableLongObjectMapOf() + @OptIn(ExperimentalFoundationApi::class) + private val isSuspendingPointerInputEnabled = + !ComposeFoundationFlags.isNonSuspendingPointerInputInCombinedClickableEnabled + private var downEvent: PointerInputChange? = null + private var longPressJob: Job? = null + private var tapJob: Job? = null + private var isSecondTap = false + private var longPressTriggered = false + private var firstTapUpTime = -1L + private var ignoreNextUp = false + + override fun createPointerInputNodeIfNeeded(): SuspendingPointerInputModifierNode? { + if (isSuspendingPointerInputEnabled) { + return SuspendingPointerInputModifierNode { + detectTapGestures( + onDoubleTap = + if (enabled && onDoubleClick != null) { + { onDoubleClick?.invoke() } + } else null, + onLongPress = + if (enabled && onLongClick != null) { + { + onLongClick?.invoke() + if (hapticFeedbackEnabled) { + currentValueOf(LocalHapticFeedback) + .performHapticFeedback(HapticFeedbackType.LongPress) + } + } + } else null, + onPress = { offset -> + if (enabled) { + handlePressInteraction(offset) + } + }, + onTap = { + if (enabled) { + onClick() + } + }, + ) + } + } + return null + } - override fun createPointerInputNodeIfNeeded() = SuspendingPointerInputModifierNode { - detectTapGestures( - onDoubleTap = - if (enabled && onDoubleClick != null) { - { onDoubleClick?.invoke() } - } else null, - onLongPress = - if (enabled && onLongClick != null) { - { + @OptIn(ExperimentalFoundationApi::class) + override fun onPointerEvent( + pointerEvent: PointerEvent, + pass: PointerEventPass, + bounds: IntSize, + ) { + super.onPointerEvent(pointerEvent, pass, bounds) + if (isSuspendingPointerInputEnabled) return + + if (pass == PointerEventPass.Main) { + if (downEvent == null) { + if (pointerEvent.isChangedToDown(requireUnconsumed = true)) { + handleDownEvent(pointerEvent.changes[0]) + } + } else { + if (pointerEvent.isDeepPress) { + handleDeepPress() + } + if (pointerEvent.changes.fastAll { it.changedToUp() }) { + // All pointers are up + handleUpEvent(pointerEvent.changes[0]) + } else { + // Other events need to be checked for consumption / bounds related + // cancellation. + handleNonUpEventIfNeeded(pointerEvent, bounds) + } + } + } else if (pass == PointerEventPass.Final) { + checkForCancellation(pointerEvent) + } + } + + @OptIn(ExperimentalFoundationApi::class) + private fun handleDownEvent(down: PointerInputChange) { + down.consume() + this.downEvent = down + + if (enabled) { + if (tapJob?.isActive == true) { + val minTime = currentValueOf(LocalViewConfiguration).doubleTapMinTimeMillis + if (down.uptimeMillis - firstTapUpTime < minTime) { + ignoreNextUp = true + // Ignore this down event, don't check for long press / emit press + // interactions + return + } else { + isSecondTap = true + tapJob?.cancel() + tapJob = null + } + } + longPressTriggered = false + if (isDelayPressesUsingGestureConsumptionEnabled) { + handlePressInteractionStart(down) + } else { + handlePressInteractionStart(down.position, false) + } + if (onLongClick != null) { + longPressJob = + coroutineScope.launch { + delay(currentValueOf(LocalViewConfiguration).longPressTimeoutMillis) onLongClick?.invoke() if (hapticFeedbackEnabled) { currentValueOf(LocalHapticFeedback) .performHapticFeedback(HapticFeedbackType.LongPress) } + longPressTriggered = true + tapJob?.cancel() + tapJob = null + longPressJob = null + } + } + } + } + + private fun handleUpEvent(up: PointerInputChange) { + up.consume() + if (enabled && !ignoreNextUp) { + firstTapUpTime = up.uptimeMillis // store uptime for double tap check + if (!longPressTriggered) { + if (isSecondTap) { + onDoubleClick?.invoke() + } else { + if (onDoubleClick != null) { + tapJob = + coroutineScope.launch { + delay(currentValueOf(LocalViewConfiguration).doubleTapTimeoutMillis) + onClick() + tapJob = null + } + } else { + onClick() } - } else null, - onPress = { offset -> - if (enabled) { - handlePressInteraction(offset) - } - }, - onTap = { - if (enabled) { - onClick() } - }, - ) + } + handlePressInteractionRelease(downEvent!!.position, indirectPointer = false) + } + this.downEvent = null + ignoreNextUp = false + isSecondTap = false + longPressJob?.cancel() + longPressJob = null + longPressTriggered = false + } + + private fun handleNonUpEventIfNeeded(pointerEvent: PointerEvent, bounds: IntSize) { + val touchPadding = getExtendedTouchPadding(bounds) + for (i in 0 until pointerEvent.changes.size) { + val change = pointerEvent.changes[i] + if (change.isConsumed || change.isOutOfBounds(bounds, touchPadding)) { + cancelPointerInput() + break + } else if (longPressTriggered) { + // Once a long press has triggered, consume all events until pointer is + // released + change.consume() + } + } + } + + private fun handleDeepPress() { + if (enabled && onLongClick != null) { + longPressJob?.cancel() + longPressJob = null + onLongClick?.invoke() + if (hapticFeedbackEnabled) { + currentValueOf(LocalHapticFeedback) + .performHapticFeedback(HapticFeedbackType.LongPress) + } + longPressTriggered = true + } + } + + private fun checkForCancellation(pointerEvent: PointerEvent) { + if (downEvent != null && !longPressTriggered) { + // 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. We ignore + // cases where the long press has already triggered, as in this case we will consume + // events ourselves until the pointer is released. + if (pointerEvent.changes.fastAny { it.isConsumed && it != downEvent }) { + // Canceled + cancelPointerInput() + } + } + } + + override fun onCancelPointerInput() { + super.onCancelPointerInput() + cancelPointerInput() + } + + private fun cancelPointerInput() { + downEvent = null + longPressJob?.cancel() + longPressJob = null + tapJob?.cancel() + tapJob = null + isSecondTap = false + longPressTriggered = false + firstTapUpTime = -1L + ignoreNextUp = false + handlePressInteractionCancel(indirectPointer = false) } fun update( @@ -1050,7 +1220,7 @@ private class CombinedClickableNode( var resetPointerInputHandling = false // onClick is captured inside a callback, not as an input to detectTapGestures, - // so no need need to reset pointer input handling + // so no need to reset pointer input handling if (this.onLongClickLabel != onLongClickLabel) { this.onLongClickLabel = onLongClickLabel @@ -1059,7 +1229,7 @@ private class CombinedClickableNode( // We capture onLongClick and onDoubleClick inside the callback, so if the lambda changes // value we don't want to reset input handling - only reset if they go from not-defined to - // defined, and vice-versa, as that is what is captured in the parameter to + // defined, and vice versa, as that is what is captured in the parameter to // detectTapGestures. if ((this.onLongClick == null) != (onLongClick == null)) { // Adding or removing longClick should cancel any existing press interactions @@ -1093,7 +1263,10 @@ private class CombinedClickableNode( onClick = onClick, ) - if (resetPointerInputHandling) resetPointerInputHandler() + if (resetPointerInputHandling) { + resetPointerInputHandler() + cancelPointerInput() + } } override fun SemanticsPropertyReceiver.applyAdditionalSemantics() { @@ -1351,6 +1524,16 @@ internal abstract class AbstractClickableNode( focusableNode.update(this.interactionSource) } + protected fun getExtendedTouchPadding(size: IntSize): Size { + // copied from SuspendingPointerInputModifierNodeImpl.extendedTouchPadding: + // TODO expose this as a new public api available outside of suspending apis b/422396609 + val minimumTouchTargetSizeDp = currentValueOf(LocalViewConfiguration).minimumTouchTargetSize + val minimumTouchTargetSize = with(requireDensity()) { minimumTouchTargetSizeDp.toSize() } + val horizontal = max(0f, minimumTouchTargetSize.width - size.width) / 2f + val vertical = max(0f, minimumTouchTargetSize.height - size.height) / 2f + return Size(horizontal, vertical) + } + override fun onIndirectPointerEvent(event: IndirectPointerEvent, pass: PointerEventPass) { initializeIndicationAndInteractionSourceIfNeeded() if (enabled) { @@ -1957,6 +2140,4 @@ private fun unsupportedIndicationExceptionMessage(indication: Indication): Strin private fun IndirectPointerInputChange.changedToUp() = !isConsumed && previousPressed && !pressed -private fun IndirectPointerInputChange.changedToDownIgnoreConsumed() = !previousPressed && pressed - private fun IndirectPointerInputChange.isMovingIgnoreConsumed() = previousPressed && pressed diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ComposeFoundationFlags.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ComposeFoundationFlags.kt index 27d780e4a7fd9..cb1e0f0efe62e 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ComposeFoundationFlags.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ComposeFoundationFlags.kt @@ -143,4 +143,12 @@ object ComposeFoundationFlags { @field:Suppress("MutableBareField") @JvmField var isNestedDraggablesTouchConflictFixEnabled = true + + /** + * With this flag on we don't use suspend pointer input as part of Modifier.combinedClickable + * implementation as an optimization. + */ + @field:Suppress("MutableBareField") + @JvmField + var isNonSuspendingPointerInputInCombinedClickableEnabled = true } From 7d3012035262391ba62614d3dacd244176a4f00c Mon Sep 17 00:00:00 2001 From: George Mount Date: Thu, 15 Jan 2026 11:23:45 -0800 Subject: [PATCH 03/21] Move SharedDrawScope and CanvasHolder into ComposeViewContext Relnote: "DrawScope and CanvasHolder can now be shared across ComposeViews when AndroidComposeUiFlags.isSharedDrawingEnabled is true." Test: existing tests Change-Id: I528e767d7f1b6a497833835472e8a8a5f91ed6ee --- compose/ui/ui/api/current.txt | 2 ++ compose/ui/ui/api/restricted_current.txt | 2 ++ .../compose/ui/graphics/vector/VectorTest.kt | 3 ++- .../ui/AndroidComposeUiFlags.android.kt | 4 ++++ .../ui/platform/AndroidComposeView.android.kt | 18 ++++++++++++++++-- .../ui/platform/ComposeViewContext.android.kt | 11 +++++++++++ 6 files changed, 37 insertions(+), 3 deletions(-) diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt index 20d660b3a9b37..2750a98720149 100644 --- a/compose/ui/ui/api/current.txt +++ b/compose/ui/ui/api/current.txt @@ -73,10 +73,12 @@ package androidx.compose.ui { @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public final class AndroidComposeUiFlags { property public boolean isSharedAccessibilityManagerEnabled; property public boolean isSharedComposeViewContextEnabled; + property public boolean isSharedDrawingEnabled; property public boolean isSharedWindowInfoEnabled; field public static final androidx.compose.ui.AndroidComposeUiFlags INSTANCE; field public static boolean isSharedAccessibilityManagerEnabled; field public static boolean isSharedComposeViewContextEnabled; + field public static boolean isSharedDrawingEnabled; field public static boolean isSharedWindowInfoEnabled; } diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt index 3bb7865ac0e93..d24d837ab4279 100644 --- a/compose/ui/ui/api/restricted_current.txt +++ b/compose/ui/ui/api/restricted_current.txt @@ -73,10 +73,12 @@ package androidx.compose.ui { @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public final class AndroidComposeUiFlags { property public boolean isSharedAccessibilityManagerEnabled; property public boolean isSharedComposeViewContextEnabled; + property public boolean isSharedDrawingEnabled; property public boolean isSharedWindowInfoEnabled; field public static final androidx.compose.ui.AndroidComposeUiFlags INSTANCE; field public static boolean isSharedAccessibilityManagerEnabled; field public static boolean isSharedComposeViewContextEnabled; + field public static boolean isSharedDrawingEnabled; field public static boolean isSharedWindowInfoEnabled; } diff --git a/compose/ui/ui/src/androidDeviceTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt b/compose/ui/ui/src/androidDeviceTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt index f68425c36daeb..c86cdad7ddb5e 100644 --- a/compose/ui/ui/src/androidDeviceTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt +++ b/compose/ui/ui/src/androidDeviceTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt @@ -1272,7 +1272,7 @@ class VectorTest { var vectorCache: ImageVectorCache? = null var vectorInCache = false var theme: Resources.Theme? = null - var refillCache = true + var refillCache by mutableStateOf(true) try { rule.setContent { val imageVectorCache = LocalImageVectorCache.current @@ -1295,6 +1295,7 @@ class VectorTest { Log.w(TAG, "device rotation unsuccessful") return } + rule.waitForIdle() val cacheMiss = vectorCache?.let { diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/AndroidComposeUiFlags.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/AndroidComposeUiFlags.android.kt index 0c7e9be77c667..f3600958c5af1 100644 --- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/AndroidComposeUiFlags.android.kt +++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/AndroidComposeUiFlags.android.kt @@ -69,4 +69,8 @@ object AndroidComposeUiFlags { @JvmField // To be removed b/479845566 var isSharedAccessibilityManagerEnabled: Boolean = true + + /** This moves DrawScope and CanvasHolder into the shared ComposeViewContext. */ + // To be removed b/479849019 + @field:Suppress("MutableBareField") @JvmField var isSharedDrawingEnabled: Boolean = true } 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 b83995a0cd5a4..4a7f48ad9e20c 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 @@ -341,7 +341,14 @@ internal class AndroidComposeView(context: Context, composeViewContext: ComposeV IndirectPointerEventPrimaryDirectionalMotionAxis? = null - override val sharedDrawScope = LayoutNodeDrawScope() + override val sharedDrawScope = + // TODO: when removing the flag, change this to a get() block + @OptIn(ExperimentalComposeUiApi::class) + if (AndroidComposeUiFlags.isSharedDrawingEnabled) { + composeViewContext.sharedDrawScope + } else { + LayoutNodeDrawScope() + } override val view: View get() = this @@ -561,7 +568,14 @@ internal class AndroidComposeView(context: Context, composeViewContext: ComposeV return null } - private val canvasHolder = CanvasHolder() + private val canvasHolder: CanvasHolder = + // TODO: when removing the flag, change this to a get() block + @OptIn(ExperimentalComposeUiApi::class) + if (AndroidComposeUiFlags.isSharedDrawingEnabled) { + composeViewContext.canvasHolder + } else { + CanvasHolder() + } override val viewConfiguration: ViewConfiguration = AndroidViewConfiguration(android.view.ViewConfiguration.get(context)) diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ComposeViewContext.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ComposeViewContext.android.kt index 5ffc32e2cc5da..5e06cf2f3373f 100644 --- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ComposeViewContext.android.kt +++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ComposeViewContext.android.kt @@ -37,6 +37,8 @@ import androidx.compose.runtime.tooling.CompositionData import androidx.compose.runtime.tooling.LocalInspectionTables import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.R +import androidx.compose.ui.graphics.CanvasHolder +import androidx.compose.ui.node.LayoutNodeDrawScope import androidx.compose.ui.res.ImageVectorCache import androidx.compose.ui.res.ResourceIdCache import androidx.compose.ui.unit.Density @@ -117,9 +119,18 @@ internal class ComposeViewContext( /** [UriHandler] provided by [LocalUriHandler] */ internal val uriHandler = AndroidUriHandler(view.context) + /** [LayoutNodeDrawScope] shared across all [ComposeView]s using this [ComposeViewContext] */ + internal val sharedDrawScope = LayoutNodeDrawScope() + /** [WindowInfo] provide by [LocalWindowInfo]. */ internal val windowInfo: LazyWindowInfo = LazyWindowInfo() + /** + * A [CanvasHolder] that can be used for all AndroidComposeViews using this + * [ComposeViewContext]. + */ + internal val canvasHolder = CanvasHolder() + /** * The number of Views that are currently attached to the view hierarchy that are using this * ComposeViewContext. From a7a57fc92ef9ba65877b153b9bc5181bb3322989 Mon Sep 17 00:00:00 2001 From: pfthomas Date: Tue, 30 Dec 2025 09:10:31 -0500 Subject: [PATCH 04/21] [SearchBar] Docked Expressive animations Test: manual RelNote: "Added rememberWithGapSearchBarState to be used with ExpandedDockedSearchBarWithGap." Change-Id: Ia336100f392864316472b9107f4ab019bf8950cb --- compose/material3/material3/api/current.txt | 8 +- .../material3/api/restricted_current.txt | 8 +- .../material3/bcv/native/current.txt | 2 +- .../material3/samples/SearchBarSamples.kt | 3 +- .../material3/SearchBarScreenshotTest.kt | 22 +- .../androidx/compose/material3/SearchBar.kt | 234 +++++++++++++++--- 6 files changed, 225 insertions(+), 52 deletions(-) diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt index ecea7063b90e9..9e67b63fe3e90 100644 --- a/compose/material3/material3/api/current.txt +++ b/compose/material3/material3/api/current.txt @@ -3040,6 +3040,7 @@ package androidx.compose.material3 { method @InaccessibleFromKotlin public androidx.compose.foundation.layout.PaddingValues getAppBarContentPadding(); method @BytecodeOnly @androidx.compose.runtime.Composable public long getCollapsedContainedSearchBarColor(androidx.compose.runtime.Composer?, int); method @BytecodeOnly public float getDockedDropdownGapSize-D9Ej5fM(); + method @BytecodeOnly @androidx.compose.runtime.Composable public long getDockedDropdownScrimColor(androidx.compose.runtime.Composer?, int); method @InaccessibleFromKotlin public androidx.compose.ui.graphics.Shape getDockedDropdownShape(); method @BytecodeOnly @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getDockedShape(androidx.compose.runtime.Composer?, int); method @BytecodeOnly @Deprecated public float getElevation-D9Ej5fM(); @@ -3062,6 +3063,7 @@ package androidx.compose.material3 { property public androidx.compose.ui.unit.Dp TonalElevation; property public androidx.compose.ui.graphics.Color collapsedContainedSearchBarColor; property public androidx.compose.ui.unit.Dp dockedDropdownGapSize; + property public androidx.compose.ui.graphics.Color dockedDropdownScrimColor; property public androidx.compose.ui.graphics.Shape dockedDropdownShape; property public androidx.compose.ui.graphics.Shape dockedShape; property public androidx.compose.ui.graphics.Color fullScreenContainedSearchBarColor; @@ -3081,8 +3083,8 @@ package androidx.compose.material3 { method @BytecodeOnly @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void DockedSearchBar-eWTbjVg(String, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function1, boolean, kotlin.jvm.functions.Function1, androidx.compose.ui.Modifier?, boolean, kotlin.jvm.functions.Function2?, kotlin.jvm.functions.Function2?, kotlin.jvm.functions.Function2?, androidx.compose.ui.graphics.Shape?, androidx.compose.material3.SearchBarColors?, float, float, androidx.compose.foundation.interaction.MutableInteractionSource?, kotlin.jvm.functions.Function3, androidx.compose.runtime.Composer?, int, int, int); method @KotlinOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ExpandedDockedSearchBar(androidx.compose.material3.SearchBarState state, kotlin.jvm.functions.Function0 inputField, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SearchBarColors colors, optional androidx.compose.ui.unit.Dp tonalElevation, optional androidx.compose.ui.unit.Dp shadowElevation, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1 content); method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ExpandedDockedSearchBar-qKj4JfE(androidx.compose.material3.SearchBarState, kotlin.jvm.functions.Function2, androidx.compose.ui.Modifier?, androidx.compose.ui.graphics.Shape?, androidx.compose.material3.SearchBarColors?, float, float, androidx.compose.ui.window.PopupProperties?, kotlin.jvm.functions.Function3, androidx.compose.runtime.Composer?, int, int); - method @KotlinOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ExpandedDockedSearchBarWithGap(androidx.compose.material3.SearchBarState state, kotlin.jvm.functions.Function0 inputField, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape dropdownShape, optional androidx.compose.ui.unit.Dp dropdownGapSize, optional androidx.compose.material3.SearchBarColors colors, optional androidx.compose.ui.unit.Dp tonalElevation, optional androidx.compose.ui.unit.Dp shadowElevation, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1 content); - method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ExpandedDockedSearchBarWithGap-3qcrL_c(androidx.compose.material3.SearchBarState, kotlin.jvm.functions.Function2, androidx.compose.ui.Modifier?, androidx.compose.ui.graphics.Shape?, float, androidx.compose.material3.SearchBarColors?, float, float, androidx.compose.ui.window.PopupProperties?, kotlin.jvm.functions.Function3, androidx.compose.runtime.Composer?, int, int); + method @KotlinOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ExpandedDockedSearchBarWithGap(androidx.compose.material3.SearchBarState state, kotlin.jvm.functions.Function0 inputField, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.ui.graphics.Shape dropdownShape, optional androidx.compose.ui.unit.Dp dropdownGapSize, optional androidx.compose.ui.graphics.Color dropdownScrimColor, optional androidx.compose.material3.SearchBarColors colors, optional androidx.compose.ui.unit.Dp tonalElevation, optional androidx.compose.ui.unit.Dp shadowElevation, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1 content); + method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ExpandedDockedSearchBarWithGap-AX2PdCw(androidx.compose.material3.SearchBarState, kotlin.jvm.functions.Function2, androidx.compose.ui.Modifier?, androidx.compose.ui.graphics.Shape?, androidx.compose.ui.graphics.Shape?, float, long, androidx.compose.material3.SearchBarColors?, float, float, androidx.compose.ui.window.PopupProperties?, kotlin.jvm.functions.Function3, androidx.compose.runtime.Composer?, int, int, int); method @KotlinOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ExpandedFullScreenContainedSearchBar(androidx.compose.material3.SearchBarState state, kotlin.jvm.functions.Function0 inputField, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape collapsedShape, optional androidx.compose.material3.SearchBarColors colors, optional androidx.compose.ui.unit.Dp tonalElevation, optional androidx.compose.ui.unit.Dp shadowElevation, optional kotlin.jvm.functions.Function0 windowInsets, optional androidx.compose.ui.window.DialogProperties properties, kotlin.jvm.functions.Function1 content); method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ExpandedFullScreenContainedSearchBar-_UtchM0(androidx.compose.material3.SearchBarState, kotlin.jvm.functions.Function2, androidx.compose.ui.Modifier?, androidx.compose.ui.graphics.Shape?, androidx.compose.material3.SearchBarColors?, float, float, kotlin.jvm.functions.Function2?, androidx.compose.ui.window.DialogProperties?, kotlin.jvm.functions.Function3, androidx.compose.runtime.Composer?, int, int); method @KotlinOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ExpandedFullScreenSearchBar(androidx.compose.material3.SearchBarState state, kotlin.jvm.functions.Function0 inputField, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape collapsedShape, optional androidx.compose.material3.SearchBarColors colors, optional androidx.compose.ui.unit.Dp tonalElevation, optional androidx.compose.ui.unit.Dp shadowElevation, optional kotlin.jvm.functions.Function0 windowInsets, optional androidx.compose.ui.window.DialogProperties properties, kotlin.jvm.functions.Function1 content); @@ -3099,6 +3101,8 @@ package androidx.compose.material3 { method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.SearchBarState rememberContainedSearchBarState(androidx.compose.material3.SearchBarValue?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.runtime.Composer?, int, int); method @KotlinOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.SearchBarState rememberSearchBarState(optional androidx.compose.material3.SearchBarValue initialValue, optional androidx.compose.animation.core.AnimationSpec animationSpecForExpand, optional androidx.compose.animation.core.AnimationSpec animationSpecForCollapse); method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.SearchBarState rememberSearchBarState(androidx.compose.material3.SearchBarValue?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.runtime.Composer?, int, int); + method @KotlinOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.SearchBarState rememberWithGapSearchBarState(optional androidx.compose.material3.SearchBarValue initialValue, optional androidx.compose.animation.core.AnimationSpec animationSpecForExpand, optional androidx.compose.animation.core.AnimationSpec animationSpecForCollapse, optional androidx.compose.animation.core.AnimationSpec animationSpecForContentFadeIn, optional androidx.compose.animation.core.AnimationSpec animationSpecForContentFadeOut); + method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.SearchBarState rememberWithGapSearchBarState(androidx.compose.material3.SearchBarValue?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.runtime.Composer?, int, int); } @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public interface SearchBarScrollBehavior { diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt index ecea7063b90e9..9e67b63fe3e90 100644 --- a/compose/material3/material3/api/restricted_current.txt +++ b/compose/material3/material3/api/restricted_current.txt @@ -3040,6 +3040,7 @@ package androidx.compose.material3 { method @InaccessibleFromKotlin public androidx.compose.foundation.layout.PaddingValues getAppBarContentPadding(); method @BytecodeOnly @androidx.compose.runtime.Composable public long getCollapsedContainedSearchBarColor(androidx.compose.runtime.Composer?, int); method @BytecodeOnly public float getDockedDropdownGapSize-D9Ej5fM(); + method @BytecodeOnly @androidx.compose.runtime.Composable public long getDockedDropdownScrimColor(androidx.compose.runtime.Composer?, int); method @InaccessibleFromKotlin public androidx.compose.ui.graphics.Shape getDockedDropdownShape(); method @BytecodeOnly @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getDockedShape(androidx.compose.runtime.Composer?, int); method @BytecodeOnly @Deprecated public float getElevation-D9Ej5fM(); @@ -3062,6 +3063,7 @@ package androidx.compose.material3 { property public androidx.compose.ui.unit.Dp TonalElevation; property public androidx.compose.ui.graphics.Color collapsedContainedSearchBarColor; property public androidx.compose.ui.unit.Dp dockedDropdownGapSize; + property public androidx.compose.ui.graphics.Color dockedDropdownScrimColor; property public androidx.compose.ui.graphics.Shape dockedDropdownShape; property public androidx.compose.ui.graphics.Shape dockedShape; property public androidx.compose.ui.graphics.Color fullScreenContainedSearchBarColor; @@ -3081,8 +3083,8 @@ package androidx.compose.material3 { method @BytecodeOnly @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void DockedSearchBar-eWTbjVg(String, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function1, boolean, kotlin.jvm.functions.Function1, androidx.compose.ui.Modifier?, boolean, kotlin.jvm.functions.Function2?, kotlin.jvm.functions.Function2?, kotlin.jvm.functions.Function2?, androidx.compose.ui.graphics.Shape?, androidx.compose.material3.SearchBarColors?, float, float, androidx.compose.foundation.interaction.MutableInteractionSource?, kotlin.jvm.functions.Function3, androidx.compose.runtime.Composer?, int, int, int); method @KotlinOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ExpandedDockedSearchBar(androidx.compose.material3.SearchBarState state, kotlin.jvm.functions.Function0 inputField, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SearchBarColors colors, optional androidx.compose.ui.unit.Dp tonalElevation, optional androidx.compose.ui.unit.Dp shadowElevation, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1 content); method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ExpandedDockedSearchBar-qKj4JfE(androidx.compose.material3.SearchBarState, kotlin.jvm.functions.Function2, androidx.compose.ui.Modifier?, androidx.compose.ui.graphics.Shape?, androidx.compose.material3.SearchBarColors?, float, float, androidx.compose.ui.window.PopupProperties?, kotlin.jvm.functions.Function3, androidx.compose.runtime.Composer?, int, int); - method @KotlinOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ExpandedDockedSearchBarWithGap(androidx.compose.material3.SearchBarState state, kotlin.jvm.functions.Function0 inputField, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape dropdownShape, optional androidx.compose.ui.unit.Dp dropdownGapSize, optional androidx.compose.material3.SearchBarColors colors, optional androidx.compose.ui.unit.Dp tonalElevation, optional androidx.compose.ui.unit.Dp shadowElevation, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1 content); - method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ExpandedDockedSearchBarWithGap-3qcrL_c(androidx.compose.material3.SearchBarState, kotlin.jvm.functions.Function2, androidx.compose.ui.Modifier?, androidx.compose.ui.graphics.Shape?, float, androidx.compose.material3.SearchBarColors?, float, float, androidx.compose.ui.window.PopupProperties?, kotlin.jvm.functions.Function3, androidx.compose.runtime.Composer?, int, int); + method @KotlinOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ExpandedDockedSearchBarWithGap(androidx.compose.material3.SearchBarState state, kotlin.jvm.functions.Function0 inputField, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.ui.graphics.Shape dropdownShape, optional androidx.compose.ui.unit.Dp dropdownGapSize, optional androidx.compose.ui.graphics.Color dropdownScrimColor, optional androidx.compose.material3.SearchBarColors colors, optional androidx.compose.ui.unit.Dp tonalElevation, optional androidx.compose.ui.unit.Dp shadowElevation, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1 content); + method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ExpandedDockedSearchBarWithGap-AX2PdCw(androidx.compose.material3.SearchBarState, kotlin.jvm.functions.Function2, androidx.compose.ui.Modifier?, androidx.compose.ui.graphics.Shape?, androidx.compose.ui.graphics.Shape?, float, long, androidx.compose.material3.SearchBarColors?, float, float, androidx.compose.ui.window.PopupProperties?, kotlin.jvm.functions.Function3, androidx.compose.runtime.Composer?, int, int, int); method @KotlinOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ExpandedFullScreenContainedSearchBar(androidx.compose.material3.SearchBarState state, kotlin.jvm.functions.Function0 inputField, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape collapsedShape, optional androidx.compose.material3.SearchBarColors colors, optional androidx.compose.ui.unit.Dp tonalElevation, optional androidx.compose.ui.unit.Dp shadowElevation, optional kotlin.jvm.functions.Function0 windowInsets, optional androidx.compose.ui.window.DialogProperties properties, kotlin.jvm.functions.Function1 content); method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ExpandedFullScreenContainedSearchBar-_UtchM0(androidx.compose.material3.SearchBarState, kotlin.jvm.functions.Function2, androidx.compose.ui.Modifier?, androidx.compose.ui.graphics.Shape?, androidx.compose.material3.SearchBarColors?, float, float, kotlin.jvm.functions.Function2?, androidx.compose.ui.window.DialogProperties?, kotlin.jvm.functions.Function3, androidx.compose.runtime.Composer?, int, int); method @KotlinOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ExpandedFullScreenSearchBar(androidx.compose.material3.SearchBarState state, kotlin.jvm.functions.Function0 inputField, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape collapsedShape, optional androidx.compose.material3.SearchBarColors colors, optional androidx.compose.ui.unit.Dp tonalElevation, optional androidx.compose.ui.unit.Dp shadowElevation, optional kotlin.jvm.functions.Function0 windowInsets, optional androidx.compose.ui.window.DialogProperties properties, kotlin.jvm.functions.Function1 content); @@ -3099,6 +3101,8 @@ package androidx.compose.material3 { method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.SearchBarState rememberContainedSearchBarState(androidx.compose.material3.SearchBarValue?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.runtime.Composer?, int, int); method @KotlinOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.SearchBarState rememberSearchBarState(optional androidx.compose.material3.SearchBarValue initialValue, optional androidx.compose.animation.core.AnimationSpec animationSpecForExpand, optional androidx.compose.animation.core.AnimationSpec animationSpecForCollapse); method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.SearchBarState rememberSearchBarState(androidx.compose.material3.SearchBarValue?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.runtime.Composer?, int, int); + method @KotlinOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.SearchBarState rememberWithGapSearchBarState(optional androidx.compose.material3.SearchBarValue initialValue, optional androidx.compose.animation.core.AnimationSpec animationSpecForExpand, optional androidx.compose.animation.core.AnimationSpec animationSpecForCollapse, optional androidx.compose.animation.core.AnimationSpec animationSpecForContentFadeIn, optional androidx.compose.animation.core.AnimationSpec animationSpecForContentFadeOut); + method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.SearchBarState rememberWithGapSearchBarState(androidx.compose.material3.SearchBarValue?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.runtime.Composer?, int, int); } @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public interface SearchBarScrollBehavior { diff --git a/compose/material3/material3/bcv/native/current.txt b/compose/material3/material3/bcv/native/current.txt index da71797c816b4..4366d536bdea6 100644 --- a/compose/material3/material3/bcv/native/current.txt +++ b/compose/material3/material3/bcv/native/current.txt @@ -3913,7 +3913,7 @@ final fun androidx.compose.material3/ElevatedSuggestionChip(kotlin/Function0, kotlin/Function2, androidx.compose.ui/Modifier?, kotlin/Boolean, kotlin/Function2?, androidx.compose.ui.graphics/Shape?, androidx.compose.material3/ChipColors?, androidx.compose.material3/ChipElevation?, androidx.compose.foundation/BorderStroke?, androidx.compose.foundation.layout/Arrangement.Horizontal?, androidx.compose.foundation.layout/PaddingValues?, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // androidx.compose.material3/ElevatedSuggestionChip|ElevatedSuggestionChip(kotlin.Function0;kotlin.Function2;androidx.compose.ui.Modifier?;kotlin.Boolean;kotlin.Function2?;androidx.compose.ui.graphics.Shape?;androidx.compose.material3.ChipColors?;androidx.compose.material3.ChipElevation?;androidx.compose.foundation.BorderStroke?;androidx.compose.foundation.layout.Arrangement.Horizontal?;androidx.compose.foundation.layout.PaddingValues?;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0] final fun androidx.compose.material3/ElevatedSuggestionChip(kotlin/Function0, kotlin/Function2, androidx.compose.ui/Modifier?, kotlin/Boolean, kotlin/Function2?, androidx.compose.ui.graphics/Shape?, androidx.compose.material3/ChipColors?, androidx.compose.material3/ChipElevation?, androidx.compose.material3/ChipBorder?, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.material3/ElevatedSuggestionChip|ElevatedSuggestionChip(kotlin.Function0;kotlin.Function2;androidx.compose.ui.Modifier?;kotlin.Boolean;kotlin.Function2?;androidx.compose.ui.graphics.Shape?;androidx.compose.material3.ChipColors?;androidx.compose.material3.ChipElevation?;androidx.compose.material3.ChipBorder?;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun androidx.compose.material3/ElevatedToggleButton(kotlin/Boolean, kotlin/Function1, androidx.compose.ui/Modifier?, kotlin/Boolean, androidx.compose.material3/ToggleButtonShapes?, androidx.compose.material3/ToggleButtonColors?, androidx.compose.material3/ButtonElevation?, androidx.compose.foundation/BorderStroke?, androidx.compose.foundation.layout/PaddingValues?, androidx.compose.foundation.interaction/MutableInteractionSource?, kotlin/Function3, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // androidx.compose.material3/ElevatedToggleButton|ElevatedToggleButton(kotlin.Boolean;kotlin.Function1;androidx.compose.ui.Modifier?;kotlin.Boolean;androidx.compose.material3.ToggleButtonShapes?;androidx.compose.material3.ToggleButtonColors?;androidx.compose.material3.ButtonElevation?;androidx.compose.foundation.BorderStroke?;androidx.compose.foundation.layout.PaddingValues?;androidx.compose.foundation.interaction.MutableInteractionSource?;kotlin.Function3;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0] -final fun androidx.compose.material3/ExpandedDockedSearchBarWithGap(androidx.compose.material3/SearchBarState, kotlin/Function2, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Shape?, androidx.compose.ui.unit/Dp, androidx.compose.material3/SearchBarColors?, androidx.compose.ui.unit/Dp, androidx.compose.ui.unit/Dp, androidx.compose.ui.window/PopupProperties?, kotlin/Function3, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.material3/ExpandedDockedSearchBarWithGap|ExpandedDockedSearchBarWithGap(androidx.compose.material3.SearchBarState;kotlin.Function2;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Shape?;androidx.compose.ui.unit.Dp;androidx.compose.material3.SearchBarColors?;androidx.compose.ui.unit.Dp;androidx.compose.ui.unit.Dp;androidx.compose.ui.window.PopupProperties?;kotlin.Function3;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] +final fun androidx.compose.material3/ExpandedDockedSearchBarWithGap(androidx.compose.material3/SearchBarState, kotlin/Function2, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Shape?, androidx.compose.ui.graphics/Shape?, androidx.compose.ui.unit/Dp, androidx.compose.ui.graphics/Color, androidx.compose.material3/SearchBarColors?, androidx.compose.ui.unit/Dp, androidx.compose.ui.unit/Dp, androidx.compose.ui.window/PopupProperties?, kotlin/Function3, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // androidx.compose.material3/ExpandedDockedSearchBarWithGap|ExpandedDockedSearchBarWithGap(androidx.compose.material3.SearchBarState;kotlin.Function2;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Shape?;androidx.compose.ui.graphics.Shape?;androidx.compose.ui.unit.Dp;androidx.compose.ui.graphics.Color;androidx.compose.material3.SearchBarColors?;androidx.compose.ui.unit.Dp;androidx.compose.ui.unit.Dp;androidx.compose.ui.window.PopupProperties?;kotlin.Function3;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0] final fun androidx.compose.material3/ExpandedFullScreenContainedSearchBar(androidx.compose.material3/SearchBarState, kotlin/Function2, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Shape?, androidx.compose.material3/SearchBarColors?, androidx.compose.ui.unit/Dp, androidx.compose.ui.unit/Dp, kotlin/Function2?, androidx.compose.ui.window/DialogProperties?, kotlin/Function3, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.material3/ExpandedFullScreenContainedSearchBar|ExpandedFullScreenContainedSearchBar(androidx.compose.material3.SearchBarState;kotlin.Function2;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Shape?;androidx.compose.material3.SearchBarColors?;androidx.compose.ui.unit.Dp;androidx.compose.ui.unit.Dp;kotlin.Function2?;androidx.compose.ui.window.DialogProperties?;kotlin.Function3;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun androidx.compose.material3/ExtendedFloatingActionButton(kotlin/Function0, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Shape?, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.material3/FloatingActionButtonElevation?, androidx.compose.foundation.interaction/MutableInteractionSource?, kotlin/Function3, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.material3/ExtendedFloatingActionButton|ExtendedFloatingActionButton(kotlin.Function0;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Shape?;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.material3.FloatingActionButtonElevation?;androidx.compose.foundation.interaction.MutableInteractionSource?;kotlin.Function3;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun androidx.compose.material3/ExtendedFloatingActionButton(kotlin/Function2, kotlin/Function2, kotlin/Function0, androidx.compose.ui/Modifier?, kotlin/Boolean, androidx.compose.ui.graphics/Shape?, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.material3/FloatingActionButtonElevation?, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.material3/ExtendedFloatingActionButton|ExtendedFloatingActionButton(kotlin.Function2;kotlin.Function2;kotlin.Function0;androidx.compose.ui.Modifier?;kotlin.Boolean;androidx.compose.ui.graphics.Shape?;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.material3.FloatingActionButtonElevation?;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SearchBarSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SearchBarSamples.kt index de4ad35e8429b..be3b20b4a8d82 100644 --- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SearchBarSamples.kt +++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SearchBarSamples.kt @@ -64,6 +64,7 @@ import androidx.compose.material3.TooltipDefaults import androidx.compose.material3.rememberContainedSearchBarState import androidx.compose.material3.rememberSearchBarState import androidx.compose.material3.rememberTooltipState +import androidx.compose.material3.rememberWithGapSearchBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier @@ -179,7 +180,7 @@ fun FullScreenSearchBarScaffoldSample() { @Composable fun DockedSearchBarScaffoldSample() { val textFieldState = rememberTextFieldState() - val searchBarState = rememberSearchBarState() + val searchBarState = rememberWithGapSearchBarState() val scope = rememberCoroutineScope() val scrollBehavior = SearchBarDefaults.enterAlwaysSearchBarScrollBehavior() val appBarWithSearchColors = SearchBarDefaults.appBarWithSearchColors() diff --git a/compose/material3/material3/src/androidDeviceTest/kotlin/androidx/compose/material3/SearchBarScreenshotTest.kt b/compose/material3/material3/src/androidDeviceTest/kotlin/androidx/compose/material3/SearchBarScreenshotTest.kt index e857b2393a9a8..53a571cdb88d7 100644 --- a/compose/material3/material3/src/androidDeviceTest/kotlin/androidx/compose/material3/SearchBarScreenshotTest.kt +++ b/compose/material3/material3/src/androidDeviceTest/kotlin/androidx/compose/material3/SearchBarScreenshotTest.kt @@ -43,6 +43,7 @@ import androidx.compose.ui.unit.dp import androidx.test.filters.LargeTest import androidx.test.filters.SdkSuppress import androidx.test.screenshot.AndroidXScreenshotTestRule +import androidx.test.screenshot.matchers.MSSIMMatcher import kotlinx.coroutines.test.StandardTestDispatcher import org.junit.Rule import org.junit.Test @@ -699,7 +700,8 @@ class SearchBarScreenshotTest(private val scheme: ColorSchemeWrapper) { @Test fun appBarWithSearch_withNavigationIconAndActions_dockedAndExpanded_withGap() { rule.setMaterialContent(scheme.colorScheme) { - val searchBarState = rememberSearchBarState(initialValue = SearchBarValue.Expanded) + val searchBarState = + rememberWithGapSearchBarState(initialValue = SearchBarValue.Expanded) val inputField = @Composable { SearchBarDefaults.InputField( @@ -728,8 +730,10 @@ class SearchBarScreenshotTest(private val scheme: ColorSchemeWrapper) { } } } + assertAgainstGolden( - "appBarWithSearch_withNavigationIconAndActions_dockedAndExpanded_withGap_${scheme.name}" + "appBarWithSearch_withNavigationIconAndActions_dockedAndExpanded_withGap_${scheme.name}", + threshold = 0.95, ) } @@ -737,7 +741,8 @@ class SearchBarScreenshotTest(private val scheme: ColorSchemeWrapper) { @Test fun appBarWithSearch_withNavigationIconAndActions_fullScreenAndExpanded_contained() { rule.setMaterialContent(scheme.colorScheme) { - val searchBarState = rememberSearchBarState(initialValue = SearchBarValue.Expanded) + val searchBarState = + rememberContainedSearchBarState(initialValue = SearchBarValue.Expanded) val appBarWithSearchColors = SearchBarDefaults.appBarWithSearchColors( searchBarColors = SearchBarDefaults.containedColors(state = searchBarState) @@ -828,8 +833,15 @@ class SearchBarScreenshotTest(private val scheme: ColorSchemeWrapper) { assertAgainstGolden("appBarWithSearch_withScrolledContainerColor_${scheme.name}") } - private fun assertAgainstGolden(goldenName: String) { - rule.onNodeWithTag(testTag).captureToImage().assertAgainstGolden(screenshotRule, goldenName) + private fun assertAgainstGolden(goldenName: String, threshold: Double = 0.98) { + rule + .onNodeWithTag(testTag, useUnmergedTree = true) + .captureToImage() + .assertAgainstGolden( + screenshotRule, + goldenName, + matcher = MSSIMMatcher(threshold = threshold), + ) } companion object { diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SearchBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SearchBar.kt index 7cbafb89611be..2279a784f255f 100644 --- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SearchBar.kt +++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SearchBar.kt @@ -39,9 +39,12 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.rememberSplineBasedDecay import androidx.compose.animation.shrinkVertically +import androidx.compose.animation.slideIn +import androidx.compose.animation.slideOut import androidx.compose.foundation.MutatorMutex import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.DraggableState import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.draggable @@ -64,6 +67,7 @@ import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.exclude +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.imePadding @@ -113,6 +117,7 @@ import androidx.compose.material3.tokens.ElevationTokens import androidx.compose.material3.tokens.FilledTextFieldTokens import androidx.compose.material3.tokens.MotionSchemeKeyTokens import androidx.compose.material3.tokens.MotionTokens +import androidx.compose.material3.tokens.ScrimTokens import androidx.compose.material3.tokens.SearchBarTokens import androidx.compose.material3.tokens.SearchViewTokens import androidx.compose.runtime.Composable @@ -211,6 +216,7 @@ import kotlin.math.max import kotlin.math.min import kotlin.math.roundToInt import kotlin.math.sign +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -458,7 +464,7 @@ fun AppBarWithSearch( } val isVisible = !state.expandsToFullScreen || - state.currentValue == SearchBarValue.Collapsed || + !state.isExpanded || state.targetValue == SearchBarValue.Expanded // prevent flickering Box(modifier = Modifier.weight(1f).alpha(if (isVisible) 1f else 0f)) { SearchBar( @@ -688,9 +694,12 @@ private fun ExpandedFullScreenSearchBarImpl( * @param inputField the input field of this search bar that allows entering a query, typically a * [SearchBarDefaults.InputField]. * @param modifier the [Modifier] to be applied to this expanded search bar. + * @param shape the shape of the container wrapping both the [inputField] and [content]. * @param dropdownShape the shape of the drop-down containing search results. * @param dropdownGapSize the size of the gap between the drop-down containing search results and * the search bar. + * @param dropdownScrimColor [Color] that will be used for the scrim behind the drop-down. Use + * [Color.Unspecified] to remove it. * @param colors [SearchBarColors] that will be used to resolve the colors used for this search bar * in different states. See [SearchBarDefaults.colors]. * @param tonalElevation when [SearchBarColors.containerColor] is [ColorScheme.surface], a @@ -710,15 +719,21 @@ fun ExpandedDockedSearchBarWithGap( state: SearchBarState, inputField: @Composable () -> Unit, modifier: Modifier = Modifier, + shape: Shape = SearchBarDefaults.dockedShape, dropdownShape: Shape = SearchBarDefaults.dockedDropdownShape, dropdownGapSize: Dp = SearchBarDefaults.dockedDropdownGapSize, + dropdownScrimColor: Color = SearchBarDefaults.dockedDropdownScrimColor, colors: SearchBarColors = SearchBarDefaults.colors(), tonalElevation: Dp = SearchBarDefaults.TonalElevation, shadowElevation: Dp = SearchBarDefaults.ShadowElevation, properties: PopupProperties = PopupProperties(focusable = true, clippingEnabled = false), content: @Composable ColumnScope.() -> Unit, ) = - ExpandedDockedSearchBarImpl(state = state, properties = properties) { focusRequester -> + ExpandedDockedSearchBarImpl( + state = state, + properties = properties, + scrimColor = dropdownScrimColor, + ) { focusRequester -> DockedSearchBarLayout( state = state, inputField = { @@ -730,7 +745,7 @@ fun ExpandedDockedSearchBarWithGap( } }, modifier = modifier, - searchBarShape = RectangleShape, + searchBarShape = shape, dropdownShape = dropdownShape, dropdownGapSize = dropdownGapSize, colors = colors, @@ -778,7 +793,11 @@ fun ExpandedDockedSearchBar( properties: PopupProperties = PopupProperties(focusable = true, clippingEnabled = false), content: @Composable ColumnScope.() -> Unit, ) = - ExpandedDockedSearchBarImpl(state = state, properties = properties) { focusRequester -> + ExpandedDockedSearchBarImpl( + state = state, + properties = properties, + scrimColor = Color.Unspecified, + ) { focusRequester -> DockedSearchBarLayout( state = state, inputField = { @@ -805,32 +824,45 @@ fun ExpandedDockedSearchBar( private fun ExpandedDockedSearchBarImpl( state: SearchBarState, properties: PopupProperties, + scrimColor: Color, content: @Composable (FocusRequester) -> Unit, ) { if (!state.isExpanded) return + val hasScrim = scrimColor != Color.Unspecified || scrimColor != Color.Transparent val positionProvider = - remember(state) { - object : PopupPositionProvider { - override fun calculatePosition( - anchorBounds: IntRect, - windowSize: IntSize, - layoutDirection: LayoutDirection, - popupContentSize: IntSize, - ): IntOffset = state.collapsedBounds.topLeft - } + object : PopupPositionProvider { + override fun calculatePosition( + anchorBounds: IntRect, + windowSize: IntSize, + layoutDirection: LayoutDirection, + popupContentSize: IntSize, + ): IntOffset = if (hasScrim) IntOffset.Zero else state.collapsedBounds.topLeft } - val scope = rememberCoroutineScope() + val onDismiss: (() -> Unit) = { scope.launch { state.animateToCollapsed() } } Popup( popupPositionProvider = positionProvider, - onDismissRequest = { scope.launch { state.animateToCollapsed() } }, + onDismissRequest = onDismiss, properties = properties, ) { val focusRequester = remember { FocusRequester() } - content(focusRequester) + if (hasScrim) { + val scrimAlpha = scrimColor.alpha * state.progress + Box( + modifier = + Modifier.fillMaxSize() + .background(scrimColor.copy(alpha = scrimAlpha)) + .clickable(onClick = onDismiss) + .offset { state.collapsedBounds.topLeft } + ) { + content(focusRequester) + } + } else { + content(focusRequester) + } // Focus the input field on the first expansion, // but no need to re-focus if the focus gets cleared. @@ -1061,8 +1093,8 @@ class SearchBarState private constructor( internal val animatable: Animatable, private val contentAnimatable: Animatable, - private val animationSpecForExpand: AnimationSpec, - private val animationSpecForCollapse: AnimationSpec, + internal val animationSpecForExpand: AnimationSpec, + internal val animationSpecForCollapse: AnimationSpec, private val animationSpecForContentFadeIn: AnimationSpec, private val animationSpecForContentFadeOut: AnimationSpec, ) { @@ -1155,7 +1187,8 @@ private constructor( * animation completes. */ val currentValue: SearchBarValue by derivedStateOf { - if (animatable.value == Collapsed) { + val threshold = 0.02f // tolerance for spring anim + if (animatable.value <= Collapsed + threshold) { SearchBarValue.Collapsed } else { SearchBarValue.Expanded @@ -1164,20 +1197,35 @@ private constructor( /** Animate the search bar to its expanded state. */ suspend fun animateToExpanded() { - animatable.animateTo(targetValue = Expanded, animationSpec = animationSpecForExpand) - contentAnimatable.animateTo( - targetValue = Expanded, - animationSpec = animationSpecForContentFadeIn, - ) + coroutineScope { + launch { + animatable.animateTo(targetValue = Expanded, animationSpec = animationSpecForExpand) + } + launch { + contentAnimatable.animateTo( + targetValue = Expanded, + animationSpec = animationSpecForContentFadeIn, + ) + } + } } /** Animate the search bar to its collapsed state. */ suspend fun animateToCollapsed() { - contentAnimatable.animateTo( - targetValue = Collapsed, - animationSpec = animationSpecForContentFadeOut, - ) - animatable.animateTo(targetValue = Collapsed, animationSpec = animationSpecForCollapse) + coroutineScope { + launch { + contentAnimatable.animateTo( + targetValue = Collapsed, + animationSpec = animationSpecForContentFadeOut, + ) + } + launch { + animatable.animateTo( + targetValue = Collapsed, + animationSpec = animationSpecForCollapse, + ) + } + } } /** @@ -1284,9 +1332,53 @@ fun rememberContainedSearchBarState( initialValue: SearchBarValue = SearchBarValue.Collapsed, animationSpecForExpand: AnimationSpec = MotionSchemeKeyTokens.FastSpatial.value(), animationSpecForCollapse: AnimationSpec = MotionSchemeKeyTokens.FastSpatial.value(), - animationSpecForContentFadeIn: AnimationSpec = MotionSchemeKeyTokens.SlowEffects.value(), - animationSpecForContentFadeOut: AnimationSpec = - MotionSchemeKeyTokens.DefaultEffects.value(), + animationSpecForContentFadeIn: AnimationSpec = AnimationForContentFadeInSpec, + animationSpecForContentFadeOut: AnimationSpec = AnimationForContentFadeOutSpec, +): SearchBarState { + return rememberSaveable( + initialValue, + animationSpecForExpand, + animationSpecForCollapse, + animationSpecForContentFadeIn, + animationSpecForContentFadeOut, + saver = + Saver( + animationSpecForExpand = animationSpecForExpand, + animationSpecForCollapse = animationSpecForCollapse, + animationSpecForContentFadeIn = animationSpecForContentFadeIn, + animationSpecForContentFadeOut = animationSpecForContentFadeOut, + ), + ) { + SearchBarState( + initialValue = initialValue, + animationSpecForExpand = animationSpecForExpand, + animationSpecForCollapse = animationSpecForCollapse, + animationSpecForContentFadeIn = animationSpecForContentFadeIn, + animationSpecForContentFadeOut = animationSpecForContentFadeOut, + ) + } +} + +/** + * Create and remember a [SearchBarState] to use in conjunction with + * [ExpandedDockedSearchBarWithGap]. + * + * @param initialValue the initial value of whether the search bar is collapsed or expanded. + * @param animationSpecForExpand the animation spec used when the search bar expands. + * @param animationSpecForCollapse the animation spec used when the search bar collapses. + * @param animationSpecForContentFadeIn the animation spec used for the content when the search bar + * expands. + * @param animationSpecForContentFadeOut the animation spec used for the content when the search bar + * collapses. + */ +@ExperimentalMaterial3Api +@Composable +fun rememberWithGapSearchBarState( + initialValue: SearchBarValue = SearchBarValue.Collapsed, + animationSpecForExpand: AnimationSpec = MotionSchemeKeyTokens.DefaultSpatial.value(), + animationSpecForCollapse: AnimationSpec = MotionSchemeKeyTokens.FastSpatial.value(), + animationSpecForContentFadeIn: AnimationSpec = AnimationForContentFadeInSpec, + animationSpecForContentFadeOut: AnimationSpec = AnimationForContentFadeOutSpec, ): SearchBarState { return rememberSaveable( initialValue, @@ -1600,6 +1692,11 @@ object SearchBarDefaults { /** Default gap size for a drop-down attached to a [DockedSearchBar]. */ val dockedDropdownGapSize: Dp = 2.dp // TODO: replace with token. + /** Default scrim color for a drop-down attached to a [DockedSearchBar]. */ + val dockedDropdownScrimColor: Color + @Composable + get() = ScrimTokens.ContainerColor.value.copy(alpha = ScrimTokens.ContainerOpacity) + /** Default padding used for [AppBarWithSearch] content */ val AppBarContentPadding = PaddingValues(all = 0.dp) @@ -1729,7 +1826,7 @@ object SearchBarDefaults { @Composable fun containedColors(state: SearchBarState): SearchBarColors { val containerColor = - if (state.currentValue == SearchBarValue.Expanded) { + if (state.isExpanded) { fullScreenContainedSearchBarColor } else { collapsedContainedSearchBarColor @@ -3163,23 +3260,55 @@ private fun DockedSearchBarLayout( content: @Composable ColumnScope.() -> Unit, ) = DockedSearchBarLayoutImpl( - shape = searchBarShape, + shape = if (dropdownShape != null) RectangleShape else searchBarShape, state = state, - inputField = inputField, + inputField = { + if (dropdownShape != null) { + Box( + modifier = + Modifier.background(color = colors.containerColor, shape = searchBarShape) + .clip(searchBarShape) + ) { + inputField() + } + } else { + inputField() + } + }, modifier = modifier, colors = if (dropdownShape != null) colors.copy(containerColor = Color.Transparent) else colors, tonalElevation = tonalElevation, shadowElevation = shadowElevation, + hasGap = dropdownGapSize != null, content = { if (dropdownShape != null) { - Column( + @Suppress("UNCHECKED_CAST") + val slideInSpec = + state.animationSpecForExpand as? FiniteAnimationSpec ?: snap() + @Suppress("UNCHECKED_CAST") + val slideOutSpec = + state.animationSpecForCollapse as? FiniteAnimationSpec ?: snap() + AnimatedVisibility( modifier = Modifier.padding(top = dropdownGapSize ?: 0.dp) .background(color = colors.containerColor, shape = dropdownShape) - .clip(dropdownShape), - content = content, - ) + .clip(dropdownShape) + .alpha(state.contentProgress), + visible = state.progress > 0.1f && state.targetValue == SearchBarValue.Expanded, + enter = + slideIn( + animationSpec = slideInSpec, + initialOffset = { IntOffset(x = 0, y = (-it.height / 2f).roundToInt()) }, + ), + exit = + slideOut( + animationSpec = slideOutSpec, + targetOffset = { IntOffset(x = 0, y = (-it.height / 2f).roundToInt()) }, + ), + ) { + Column(content = content) + } } else { Column { HorizontalDivider(color = colors.dividerColor) @@ -3199,6 +3328,7 @@ private fun DockedSearchBarLayoutImpl( colors: SearchBarColors, tonalElevation: Dp, shadowElevation: Dp, + hasGap: Boolean, content: @Composable () -> Unit, ) { val scope = rememberCoroutineScope() @@ -3213,13 +3343,23 @@ private fun DockedSearchBarLayoutImpl( modifier = modifier.imePadding(), ) { val windowContainerHeight = getWindowContainerHeight() - val maxHeight = windowContainerHeight * DockedExpandedTableMaxHeightScreenRatio + val maxHeightScreenRatio = + if (hasGap) { + DockedExpandedWithGapTableMaxHeightScreenRatio + } else { + DockedExpandedTableMaxHeightScreenRatio + } + val maxHeight = windowContainerHeight * maxHeightScreenRatio val minHeight = DockedExpandedTableMinHeight.coerceAtMost(maxHeight) Layout(contents = listOf(inputField, content)) { measurables, baseConstraints -> val (inputFieldMeasurables, contentMeasurables) = measurables val constraintMaxHeight = - lerp(state.collapsedBounds.height, maxHeight.roundToPx(), state.progress) + lerp( + state.collapsedBounds.height, + maxHeight.roundToPx(), + state.animatable.value.coerceAtLeast(0f), + ) val constraints = baseConstraints.constrain( Constraints( @@ -3638,6 +3778,7 @@ internal val AppBarWithSearchVerticalPadding = 4.dp private val FullScreenExpandedHorizontalPadding = 8.dp internal val DockedExpandedTableMinHeight: Dp = 240.dp private const val DockedExpandedTableMaxHeightScreenRatio: Float = 2f / 3f +private const val DockedExpandedWithGapTableMaxHeightScreenRatio: Float = 1f / 2f internal val SearchBarMinWidth: Dp = 360.dp internal val SearchBarMaxWidth: Dp = 720.dp internal val SearchBarVerticalPadding: Dp = 8.dp @@ -3685,3 +3826,14 @@ private val DockedEnterTransition: EnterTransition = fadeIn(AnimationEnterFloatSpec) + expandVertically(AnimationEnterSizeSpec) private val DockedExitTransition: ExitTransition = fadeOut(AnimationExitFloatSpec) + shrinkVertically(AnimationExitSizeSpec) +private val AnimationForContentFadeInSpec: FiniteAnimationSpec = + tween( + durationMillis = MotionTokens.DurationShort2.toInt(), + delayMillis = MotionTokens.DurationShort1.toInt(), + easing = MotionTokens.EasingStandardAccelerateCubicBezier, + ) +private val AnimationForContentFadeOutSpec: FiniteAnimationSpec = + tween( + durationMillis = MotionTokens.DurationShort2.toInt(), + easing = MotionTokens.EasingStandardDecelerateCubicBezier, + ) From 760d2c9a2e41b4e25bde0e26f1a228991a27bbbb Mon Sep 17 00:00:00 2001 From: Samuel Freilich Date: Wed, 4 Feb 2026 14:06:08 -0500 Subject: [PATCH 05/21] Ink: Sync from internal state * Further work on custom shape workflow API * Adjust some annotations and comments for RestrictTo annotations * Add README.md files for Ink library and modules * Factor out callback for constructing a color long from native code to avoid an implicit circular dependency, adjust Proguard config to deal with that * Start adding annotation metadata to brush fields * Rename BrushBehavior.DampingSource to ProgressDomain * Add BrushBehavior.IntegralNode * Move stock brush definitions to Ink native implementation * Remove unreleased pencil brush from stock brushes for now * Add missing JvmOverloads to expose zero-argument versions of CanvasStrokeRenderer.create and InProgressStroke.updateShape (missed due to a now-fixed Metalava bug). Test: Presubmit Relnote: Expose zero-argument version of CanvasStrokeRenderer.create and InProgressStroke.updateShape to Java Change-Id: Id80a171f1c2c71f3ef7fa309c4aad44c4300867d Fix: 481367984, 479886232 Bug: 457715938 --- ink/README.md | 6 + ink/ink-authoring-compose/README.md | 5 + .../compose/InProgressStrokesTest.kt | 31 - ink/ink-authoring/README.md | 4 + .../internal/InProgressStrokesManagerTest.kt | 60 ++ .../internal/MutableBoxTransformTest.kt | 1 + .../ink/authoring/latency/LatencyDataTest.kt | 1 + .../authoring/latency/LatencyDataTestUtil.kt | 2 +- .../ink/authoring/InProgressShapesView.kt | 11 +- .../ink/authoring/InProgressStrokesView.kt | 15 +- .../ink/authoring/InkInProgressShape.kt | 43 +- .../ink/authoring/InkShapeWorkflow.kt | 2 +- .../ink/authoring/StrokeGestureCallback.kt | 4 +- .../ink/authoring/latency/LatencyData.kt | 31 +- .../authoring/latency/LatencyDataCallback.kt | 2 +- .../FixedProbabilityLatencyAggregator.kt | 2 +- .../aggregators/HistogramLatencyAggregator.kt | 2 +- .../latency/aggregators/LatencyAggregator.kt | 2 +- .../PercentileLatencyAggregator.kt | 2 +- .../RateLimitedLatencyAggregator.kt | 2 +- .../authoring/testing/InputStreamBuilder.kt | 2 +- .../testing/MultiTouchInputBuilder.kt | 2 +- ink/ink-brush-compose/README.md | 5 + ink/ink-brush/README.md | 13 + .../brush/StockTextureBitmapStore.android.kt | 19 +- .../drawable-nodpi/pencil_background_v1.png | Bin 188299 -> 0 bytes .../kotlin/androidx/ink/brush/Brush.kt | 20 - .../androidx/ink/brush/BrushBehavior.kt | 229 ++++-- .../kotlin/androidx/ink/brush/BrushCoat.kt | 12 +- .../kotlin/androidx/ink/brush/BrushFamily.kt | 130 ++-- .../kotlin/androidx/ink/brush/BrushPaint.kt | 3 +- .../kotlin/androidx/ink/brush/BrushTip.kt | 2 +- .../androidx/ink/brush/ColorExtensions.kt | 35 +- .../androidx/ink/brush/ColorFunction.kt | 6 +- .../androidx/ink/brush/EasingFunction.kt | 2 +- .../kotlin/androidx/ink/brush/StockBrushes.kt | 695 ++---------------- .../brush/TextureAnimationProgressHelper.kt | 20 +- .../ink/brush/color/colorspace/ColorSpace.kt | 4 +- .../ink/brush/color/colorspace/ColorSpaces.kt | 3 +- .../ink/brush/color/colorspace/Rgb.kt | 2 +- .../brush/TextureBufferedImageStore.jvm.kt | 2 +- .../androidx/ink/brush/BrushBehaviorTest.kt | 195 ++++- .../androidx/ink/brush/BrushFamilyTest.kt | 40 +- .../kotlin/androidx/ink/brush/BrushTest.kt | 56 +- ink/ink-geometry-compose/README.md | 5 + ink/ink-geometry/README.md | 5 + .../androidx/ink/geometry/BoxAccumulator.kt | 2 +- .../geometry/ParallelogramInterfaceTest.kt | 274 ++----- ink/ink-nativeloader/README.md | 4 + ink/ink-nativeloader/ink_jni.pgcfg | 4 +- ink/ink-rendering/README.md | 5 + ink/ink-rendering/api/current.txt | 2 + ink/ink-rendering/api/restricted_current.txt | 2 + .../canvas/StockBrushesConsistencyTest.kt | 1 - .../android/canvas/StockBrushesTest.kt | 26 +- .../android/canvas/StockBrushesTestHelper.kt | 2 - .../android/canvas/CanvasStrokeRenderer.kt | 10 +- .../canvas/internal/CanvasMeshRenderer.kt | 3 - .../internal/CanvasStrokeUnifiedRenderer.kt | 5 - ink/ink-storage/README.md | 10 + .../src/commonMain/proto/brush_family.proto | 64 +- .../androidx/ink/storage/DecompressedBytes.kt | 8 +- ink/ink-strokes/README.md | 6 + ink/ink-strokes/api/current.txt | 1 + ink/ink-strokes/api/restricted_current.txt | 1 + .../androidx/ink/strokes/InProgressStroke.kt | 17 +- .../androidx/ink/strokes/StrokeInputBatch.kt | 6 +- .../ink/strokes/testing/StrokeTestHelper.kt | 2 +- 68 files changed, 1017 insertions(+), 1173 deletions(-) create mode 100644 ink/README.md create mode 100644 ink/ink-authoring-compose/README.md create mode 100644 ink/ink-authoring/README.md create mode 100644 ink/ink-brush-compose/README.md create mode 100644 ink/ink-brush/README.md delete mode 100644 ink/ink-brush/src/androidMain/res/drawable-nodpi/pencil_background_v1.png create mode 100644 ink/ink-geometry-compose/README.md create mode 100644 ink/ink-geometry/README.md create mode 100644 ink/ink-nativeloader/README.md create mode 100644 ink/ink-rendering/README.md create mode 100644 ink/ink-storage/README.md create mode 100644 ink/ink-strokes/README.md diff --git a/ink/README.md b/ink/README.md new file mode 100644 index 0000000000000..edf8badfaa221 --- /dev/null +++ b/ink/README.md @@ -0,0 +1,6 @@ +# Ink Jetpack + +Ink supports beautiful and low-latency freehand drawing for Android apps. See +[API documentation](https://developer.android.com/jetpack/androidx/releases/ink) +for more detail. Ink Jetpack is implemented on top of a core cross-platform C++ +implementation, published [on GitHub](https://github.com/google/ink). diff --git a/ink/ink-authoring-compose/README.md b/ink/ink-authoring-compose/README.md new file mode 100644 index 0000000000000..01ee04c762354 --- /dev/null +++ b/ink/ink-authoring-compose/README.md @@ -0,0 +1,5 @@ +# Ink Authoring Compose Module + +This module contains logic for creating freehand drawing UI using Ink with +Jetpack Compose. Compose users should use this instead of the base `authoring` +module. diff --git a/ink/ink-authoring-compose/src/androidDeviceTest/kotlin/androidx/ink/authoring/compose/InProgressStrokesTest.kt b/ink/ink-authoring-compose/src/androidDeviceTest/kotlin/androidx/ink/authoring/compose/InProgressStrokesTest.kt index 23e89150e55ca..69aeb5edf2847 100644 --- a/ink/ink-authoring-compose/src/androidDeviceTest/kotlin/androidx/ink/authoring/compose/InProgressStrokesTest.kt +++ b/ink/ink-authoring-compose/src/androidDeviceTest/kotlin/androidx/ink/authoring/compose/InProgressStrokesTest.kt @@ -32,7 +32,6 @@ import androidx.ink.brush.Brush import androidx.ink.brush.ExperimentalInkCustomBrushApi import androidx.ink.brush.StockBrushes import androidx.ink.brush.StockBrushes.MarkerVersion -import androidx.ink.brush.StockTextureBitmapStore import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso.onIdle import androidx.test.ext.junit.rules.ActivityScenarioRule @@ -404,36 +403,6 @@ class InProgressStrokesTest { } } - @Test - fun downEvent_withTextureBitmapStore_showsPencilStrokeWithNoCallback() { - val stylusInputStream = - InputStreamBuilder.stylusLine(startX = 25F, startY = 25F, endX = 105F, endY = 205F) - activityScenarioRule.scenario.onActivity { activity -> - activity.init( - nextBrush = { - Brush.createWithColorIntArgb( - StockBrushes.pencilUnstable, - AVOCADO_GREEN, - 25F, - 0.1F, - ) - }, - textureBitmapStore = StockTextureBitmapStore(activity.resources), - ) - } - yieldingSleep() - - activityScenarioRule.scenario.onActivity { activity -> - activity.rootView.dispatchTouchEvent(stylusInputStream.getDownEvent()) - } - - // Single dot. - assertThatTakingScreenshotMatchesGolden("down_with_pencil_texture") - activityScenarioRule.scenario.onActivity { activity -> - assertThat(activity.finishedStrokeCohorts).isEmpty() - } - } - /** * Waits for actions to complete, both on the render thread and the UI thread, for a specified * period of time. The default time is 1 second. diff --git a/ink/ink-authoring/README.md b/ink/ink-authoring/README.md new file mode 100644 index 0000000000000..67380ca4ec1b6 --- /dev/null +++ b/ink/ink-authoring/README.md @@ -0,0 +1,4 @@ +# Ink Authoring Module + +This module contains logic for creating freehand drawing UI using Android Views +directly. Jetpack Compose users should use the Compose-specific module. diff --git a/ink/ink-authoring/src/androidDeviceTest/kotlin/androidx/ink/authoring/internal/InProgressStrokesManagerTest.kt b/ink/ink-authoring/src/androidDeviceTest/kotlin/androidx/ink/authoring/internal/InProgressStrokesManagerTest.kt index 1ff47bf810eb3..f4ad8afe7e143 100644 --- a/ink/ink-authoring/src/androidDeviceTest/kotlin/androidx/ink/authoring/internal/InProgressStrokesManagerTest.kt +++ b/ink/ink-authoring/src/androidDeviceTest/kotlin/androidx/ink/authoring/internal/InProgressStrokesManagerTest.kt @@ -1730,6 +1730,66 @@ internal class InProgressStrokesManagerTest { manager.cancelStroke(inProgressStrokeId, cancelEvent) } + @Test + fun cancelStroke_whenFinishedStrokesButNoneStillInProgress_shouldCallStrokesFinishedListener() { + val latencyDataRecorder = LatencyDataRecorder() + val clock = FakeClock() + val (manager, renderHelper, runUiThreadToEndOfFrame) = + makeAsyncManager(latencyDataRecorder, clock) + val finishedStrokeIds = mutableListOf() + manager.addListener( + object : InProgressStrokesManager.Listener { + override fun onAllStrokesFinished( + strokes: Map> + ) { + finishedStrokeIds.addAll(strokes.keys) + } + } + ) + + // Start two strokes. + val downInput = + StrokeInput.create( + x = 10f, + y = 20f, + toolType = InputToolType.TOUCH, + elapsedTimeMillis = 0, + ) + val firstStrokeId = manager.startStroke(downInput, FakeShapeSpec(), Matrix()) + renderHelper.runRenderThreadToIdle() + runUiThreadToEndOfFrame() + clock.advanceByMillis(1000) + val secondStrokeId = manager.startStroke(downInput, FakeShapeSpec(), Matrix()) + renderHelper.runRenderThreadToIdle() + runUiThreadToEndOfFrame() + clock.advanceByMillis(1000) + + // Finish the first stroke before canceling the second. + val upInput = + StrokeInput.create( + x = 50f, + y = 60f, + toolType = InputToolType.TOUCH, + elapsedTimeMillis = 2000, + ) + manager.finishStroke(upInput, firstStrokeId) + renderHelper.runRenderThreadToIdle() + runUiThreadToEndOfFrame() + clock.advanceByMillis(1000) + + // Before the second stroke is canceled, there is still a stroke in progress, so the + // finished + // first stroke shouldn't be handed off yet. + assertThat(finishedStrokeIds).isEmpty() + manager.cancelStroke(secondStrokeId, event = null) + renderHelper.runRenderThreadToIdle() + runUiThreadToEndOfFrame() + + // Only the first stroke finished successfully, but once the second stroke was canceled the + // handoff callback should have been made. + assertThat(finishedStrokeIds).containsExactly(firstStrokeId).inOrder() + } + @Test fun flush_whenNoStrokesInProgress_returnsWithoutCallingStrokesFinishedListener() { val latencyDataRecorder = LatencyDataRecorder() diff --git a/ink/ink-authoring/src/androidDeviceTest/kotlin/androidx/ink/authoring/internal/MutableBoxTransformTest.kt b/ink/ink-authoring/src/androidDeviceTest/kotlin/androidx/ink/authoring/internal/MutableBoxTransformTest.kt index 7142a21e9b42c..a8bf8cd4bc0a7 100644 --- a/ink/ink-authoring/src/androidDeviceTest/kotlin/androidx/ink/authoring/internal/MutableBoxTransformTest.kt +++ b/ink/ink-authoring/src/androidDeviceTest/kotlin/androidx/ink/authoring/internal/MutableBoxTransformTest.kt @@ -31,6 +31,7 @@ class MutableBoxTransformTest { private val floatTolerance = 0.001F + @Test fun transform_whenIdentity_resultMatchesOriginal() { val rect = MutableBox().populateFromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(3F, 4F)) diff --git a/ink/ink-authoring/src/androidDeviceTest/kotlin/androidx/ink/authoring/latency/LatencyDataTest.kt b/ink/ink-authoring/src/androidDeviceTest/kotlin/androidx/ink/authoring/latency/LatencyDataTest.kt index ead405ab8dd05..84feaddb6d68f 100644 --- a/ink/ink-authoring/src/androidDeviceTest/kotlin/androidx/ink/authoring/latency/LatencyDataTest.kt +++ b/ink/ink-authoring/src/androidDeviceTest/kotlin/androidx/ink/authoring/latency/LatencyDataTest.kt @@ -177,6 +177,7 @@ class LatencyDataTest { .isEqualTo(LatencyData.EventAction.UNKNOWN) } + @Test fun eventActionFromMotionEvent_predictedOnlyAppliesToMove() { assertThat( LatencyData.EventAction.fromMotionEvent( diff --git a/ink/ink-authoring/src/androidDeviceTest/kotlin/androidx/ink/authoring/latency/LatencyDataTestUtil.kt b/ink/ink-authoring/src/androidDeviceTest/kotlin/androidx/ink/authoring/latency/LatencyDataTestUtil.kt index a3d62ecdd50f5..5b77805ab3058 100644 --- a/ink/ink-authoring/src/androidDeviceTest/kotlin/androidx/ink/authoring/latency/LatencyDataTestUtil.kt +++ b/ink/ink-authoring/src/androidDeviceTest/kotlin/androidx/ink/authoring/latency/LatencyDataTestUtil.kt @@ -20,7 +20,7 @@ import androidx.ink.authoring.ExperimentalLatencyDataApi import com.google.common.truth.Correspondence @ExperimentalLatencyDataApi -public val latencyDataEqual: Correspondence = +val latencyDataEqual: Correspondence = Correspondence.from( { actual: LatencyData?, expected: LatencyData? -> if (expected == null || actual == null) return@from actual == expected diff --git a/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/InProgressShapesView.kt b/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/InProgressShapesView.kt index ac23823291639..b260695a35827 100644 --- a/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/InProgressShapesView.kt +++ b/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/InProgressShapesView.kt @@ -195,7 +195,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr * minimize the amount of computation in this callback, and should also avoid allocations (since * allocation may trigger the garbage collector). */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi @ExperimentalLatencyDataApi public fun getLatencyDataCallback(): LatencyDataCallback? { return latencyDataCallback @@ -207,7 +207,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr * * See [getLatencyDataCallback] */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi @ExperimentalLatencyDataApi public fun setLatencyDataCallback(value: LatencyDataCallback?) { latencyDataCallback = value @@ -775,7 +775,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr * This API is experimental for now, as one approach to address start-of-shape latency for fast * subsequent shapes. */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi public fun requestHandoff() { initializedState?.inProgressStrokesManager?.requestImmediateHandoff() } @@ -804,7 +804,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr * configurations support flushing, and flushing is best effort, so this is not guaranteed to * return `true`. */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi + @JvmOverloads + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi public fun flush( timeout: Long, timeoutUnit: TimeUnit, @@ -825,7 +826,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr * In some ways this is similar to [flush], which is intended for production use in certain * circumstances. */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi @VisibleForTesting public fun sync(timeout: Long, timeoutUnit: TimeUnit) { // Nothing to sync if it's not initialized. diff --git a/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/InProgressStrokesView.kt b/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/InProgressStrokesView.kt index 77bb2930a523c..2ae3410581056 100644 --- a/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/InProgressStrokesView.kt +++ b/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/InProgressStrokesView.kt @@ -122,8 +122,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr * the first call to [startStroke] or [eagerInit]. If this is set to a non-default value, the * value of [textureBitmapStore] is ignored. */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi - @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi + @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi @Deprecated( "For a non-self-overlapping highlighter, pass SelfOverlap.DISCARD to the selfOverlap " + "parameter of StockBrushes.highlighter." @@ -190,7 +190,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr * should minimize the amount of computation in this callback, and should also avoid allocations * (since allocation may trigger the garbage collector). */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi @ExperimentalLatencyDataApi public fun getLatencyDataCallback(): LatencyDataCallback? = inProgressShapesView.getLatencyDataCallback() @@ -201,7 +201,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr * * See [getLatencyDataCallback] */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi @ExperimentalLatencyDataApi public fun setLatencyDataCallback(value: LatencyDataCallback?): Unit = inProgressShapesView.setLatencyDataCallback(value) @@ -499,7 +499,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr * This API is experimental for now, as one approach to address start-of-stroke latency for fast * subsequent strokes. */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi public fun requestHandoff(): Unit = inProgressShapesView.requestHandoff() /** @@ -526,7 +526,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr * configurations support flushing, and flushing is best effort, so this is not guaranteed to * return `true`. */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi + @JvmOverloads public fun flush( timeout: Long, timeoutUnit: TimeUnit, @@ -543,7 +544,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr * In some ways this is similar to [flush], which is intended for production use in certain * circumstances. */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi @VisibleForTesting public fun sync(timeout: Long, timeoutUnit: TimeUnit): Unit = inProgressShapesView.sync(timeout, timeoutUnit) diff --git a/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/InkInProgressShape.kt b/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/InkInProgressShape.kt index 44f0f14f20337..b084d02870f46 100644 --- a/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/InkInProgressShape.kt +++ b/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/InkInProgressShape.kt @@ -31,14 +31,26 @@ import kotlin.random.Random /** * An implementation of [InProgressShape] that simply wraps [androidx.ink.strokes.InProgressStroke]. */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi +@OptIn(ExperimentalInkCustomBrushApi::class) +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi @ExperimentalCustomShapeWorkflowApi public class InkInProgressShape : InProgressShape { internal val inProgressStroke = InProgressStroke() + private var brush: Brush? = null + private var noiseSeed: Int = Int.MIN_VALUE + + /** + * When enabled, the same integer random noise seed is kept across calls to [start] and + * [prepareToRecycle]. This isn't needed for standard Ink behavior, but can be useful when this + * [InProgressShape] is delegated to in the implementation of another [InProgressShape]. The + * default behavior is for a new noise seed to be used each time. + */ + @get:JvmName("shouldPreserveNoiseSeed") public var shouldPreserveNoiseSeed: Boolean = false + private var shapeChangesWithTime = false - internal var textureAnimationDurationMillis: Long = -Long.MIN_VALUE + internal var textureAnimationDurationMillis: Long = Long.MIN_VALUE private set /** Whether this shape has been canceled. Primarily tracked for defensive coding purposes. */ @@ -66,9 +78,25 @@ public class InkInProgressShape : InProgressShape { */ private val scratchBoxAccumulator = BoxAccumulator() + private fun resetState() { + startSystemElapsedTimeMillis = Long.MIN_VALUE + lastUpdateSystemElapsedTimeMillis = Long.MIN_VALUE + updateSinceResetUpdatedRegion = false + cancelSinceResetUpdatedRegion = false + canceled = false + textureAnimationDurationMillis = Long.MIN_VALUE + shapeChangesWithTime = false + inProgressStroke.clear() + } + @OptIn(ExperimentalInkCustomBrushApi::class) override fun start(shapeSpec: Brush, systemElapsedTimeMillis: Long) { - inProgressStroke.start(brush = shapeSpec, noiseSeed = Random.Default.nextInt()) + resetState() + this.brush = shapeSpec + if (!shouldPreserveNoiseSeed) { + this.noiseSeed = Random.Default.nextInt() + } + inProgressStroke.start(brush = shapeSpec, noiseSeed = noiseSeed) startSystemElapsedTimeMillis = systemElapsedTimeMillis shapeChangesWithTime = inProgressStroke.changesWithTime() textureAnimationDurationMillis = @@ -173,13 +201,6 @@ public class InkInProgressShape : InProgressShape { } override fun prepareToRecycle() { - startSystemElapsedTimeMillis = Long.MIN_VALUE - lastUpdateSystemElapsedTimeMillis = Long.MIN_VALUE - updateSinceResetUpdatedRegion = false - cancelSinceResetUpdatedRegion = false - canceled = false - textureAnimationDurationMillis = -Long.MIN_VALUE - shapeChangesWithTime = false - inProgressStroke.clear() + resetState() } } diff --git a/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/InkShapeWorkflow.kt b/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/InkShapeWorkflow.kt index 2ad5122f94dad..0b35f4c7751ed 100644 --- a/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/InkShapeWorkflow.kt +++ b/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/InkShapeWorkflow.kt @@ -25,7 +25,7 @@ import androidx.ink.strokes.Stroke * a [androidx.ink.brush.TextureBitmapStore] here. */ @ExperimentalCustomShapeWorkflowApi -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi public class InkShapeWorkflow(customRendererFactory: () -> CanvasStrokeRenderer) : ShapeWorkflow { diff --git a/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/StrokeGestureCallback.kt b/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/StrokeGestureCallback.kt index 6f79a3b315c7e..a50840aaae890 100644 --- a/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/StrokeGestureCallback.kt +++ b/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/StrokeGestureCallback.kt @@ -103,7 +103,7 @@ import androidx.input.motionprediction.MotionEventPredictor * @param isRestrictedToSingleShape If `true`, then only the first pointer should be treated as a * shape. */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi @ExperimentalCustomShapeWorkflowApi public class ShapeGestureCallback( private val inProgressShapesView: InProgressShapesView, @@ -239,7 +239,7 @@ public class ShapeGestureCallback( * @param isRestrictedToSingleStroke If `true`, then only the first pointer should be treated as a * stroke. */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi public class StrokeGestureCallback( private val inProgressStrokesView: InProgressStrokesView, public var brushForNewStrokes: Brush, diff --git a/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/latency/LatencyData.kt b/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/latency/LatencyData.kt index 2f91c057708ee..2f57621954203 100644 --- a/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/latency/LatencyData.kt +++ b/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/latency/LatencyData.kt @@ -28,7 +28,7 @@ import androidx.ink.authoring.InProgressStrokeId * Timestamps are in the [System.nanoTime] timebase, which is nanoseconds since system boot, except * for deep sleep time. */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi @ExperimentalLatencyDataApi public class LatencyData { @@ -212,12 +212,12 @@ public class LatencyData { } public companion object { - public val UNKNOWN: StrokeAction = StrokeAction() - public val START: StrokeAction = StrokeAction() - public val ADD: StrokeAction = StrokeAction() - public val PREDICTED_ADD: StrokeAction = StrokeAction() - public val FINISH: StrokeAction = StrokeAction() - public val CANCEL: StrokeAction = StrokeAction() + @JvmField public val UNKNOWN: StrokeAction = StrokeAction() + @JvmField public val START: StrokeAction = StrokeAction() + @JvmField public val ADD: StrokeAction = StrokeAction() + @JvmField public val PREDICTED_ADD: StrokeAction = StrokeAction() + @JvmField public val FINISH: StrokeAction = StrokeAction() + @JvmField public val CANCEL: StrokeAction = StrokeAction() } } @@ -236,13 +236,16 @@ public class LatencyData { public companion object { // Identity of these singleton constants comes from their addresses alone. - public val UNKNOWN: EventAction = EventAction() - public val DOWN: EventAction = EventAction() - public val MOVE: EventAction = EventAction() - public val PREDICTED_MOVE: EventAction = EventAction() - public val UP: EventAction = EventAction() - public val CANCEL: EventAction = EventAction() + @JvmField public val UNKNOWN: EventAction = EventAction() + @JvmField public val DOWN: EventAction = EventAction() + @JvmField public val MOVE: EventAction = EventAction() + @JvmField public val PREDICTED_MOVE: EventAction = EventAction() + @JvmField public val UP: EventAction = EventAction() + @JvmField public val CANCEL: EventAction = EventAction() + /** Returns the [EventAction] corresponding to the given [MotionEvent]. */ + @JvmOverloads + @JvmStatic public fun fromMotionEvent( event: MotionEvent, predicted: Boolean = false, @@ -266,6 +269,6 @@ public class LatencyData { } public companion object { - public val UNKNOWN_STROKE_ID: InProgressStrokeId = InProgressStrokeId.create() + @JvmField public val UNKNOWN_STROKE_ID: InProgressStrokeId = InProgressStrokeId.create() } } diff --git a/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/latency/LatencyDataCallback.kt b/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/latency/LatencyDataCallback.kt index 19ffefa2dcfc0..923a2265be3d7 100644 --- a/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/latency/LatencyDataCallback.kt +++ b/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/latency/LatencyDataCallback.kt @@ -20,7 +20,7 @@ import androidx.annotation.RestrictTo import androidx.annotation.UiThread import androidx.ink.authoring.ExperimentalLatencyDataApi -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi @ExperimentalLatencyDataApi public fun interface LatencyDataCallback { /** A callback invoked once per input event to send [LatencyData] to a client. */ diff --git a/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/latency/aggregators/FixedProbabilityLatencyAggregator.kt b/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/latency/aggregators/FixedProbabilityLatencyAggregator.kt index bdb60853f0ead..9168fc3e840a2 100644 --- a/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/latency/aggregators/FixedProbabilityLatencyAggregator.kt +++ b/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/latency/aggregators/FixedProbabilityLatencyAggregator.kt @@ -82,7 +82,7 @@ import kotlinx.coroutines.plus * so the result is valid only for values of `t` that are multiples of `1/r` seconds. In particular, * the result is negative if `t < 1/r`, but the actual probability is 0. */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi @ExperimentalLatencyDataApi public class FixedProbabilityLatencyAggregator private constructor( diff --git a/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/latency/aggregators/HistogramLatencyAggregator.kt b/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/latency/aggregators/HistogramLatencyAggregator.kt index 25e4e4e7f6dda..494c1b457a356 100644 --- a/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/latency/aggregators/HistogramLatencyAggregator.kt +++ b/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/latency/aggregators/HistogramLatencyAggregator.kt @@ -71,7 +71,7 @@ import kotlinx.coroutines.sync.withLock * } * ``` */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi @ExperimentalLatencyDataApi public class HistogramLatencyAggregator private constructor(private val implementationHelper: ImplementationHelper) : LatencyAggregator { diff --git a/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/latency/aggregators/LatencyAggregator.kt b/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/latency/aggregators/LatencyAggregator.kt index 67455e154289e..45af2382fdaa3 100644 --- a/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/latency/aggregators/LatencyAggregator.kt +++ b/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/latency/aggregators/LatencyAggregator.kt @@ -35,7 +35,7 @@ import kotlinx.coroutines.Job * scope passed into the factory function for the chosen concrete subclass, or call * `job().cancelAndJoin()`. */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi @ExperimentalLatencyDataApi public interface LatencyAggregator { /** diff --git a/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/latency/aggregators/PercentileLatencyAggregator.kt b/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/latency/aggregators/PercentileLatencyAggregator.kt index eacfbe32ab2e9..bd69ea5d98b2b 100644 --- a/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/latency/aggregators/PercentileLatencyAggregator.kt +++ b/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/latency/aggregators/PercentileLatencyAggregator.kt @@ -61,7 +61,7 @@ import kotlinx.coroutines.runBlocking * } * ``` */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi @ExperimentalLatencyDataApi public class PercentileLatencyAggregator private constructor(private val implementationHelper: ImplementationHelper) : LatencyAggregator { diff --git a/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/latency/aggregators/RateLimitedLatencyAggregator.kt b/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/latency/aggregators/RateLimitedLatencyAggregator.kt index 1f9e3aa909234..934b47b3610e3 100644 --- a/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/latency/aggregators/RateLimitedLatencyAggregator.kt +++ b/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/latency/aggregators/RateLimitedLatencyAggregator.kt @@ -52,7 +52,7 @@ import kotlinx.coroutines.runBlocking * } * ``` */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi @ExperimentalLatencyDataApi public class RateLimitedLatencyAggregator private constructor(private val implementationHelper: ImplementationHelper) : LatencyAggregator { diff --git a/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/testing/InputStreamBuilder.kt b/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/testing/InputStreamBuilder.kt index 9d10a250117ae..f2c6c9309de20 100644 --- a/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/testing/InputStreamBuilder.kt +++ b/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/testing/InputStreamBuilder.kt @@ -44,7 +44,7 @@ import androidx.annotation.VisibleForTesting * continue to generalize that utility there may not be much need to maintain this separately. */ @VisibleForTesting -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi public class InputStreamBuilder( private val streamToolType: Int = MotionEvent.TOOL_TYPE_STYLUS, private val buttons: Int = 0, diff --git a/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/testing/MultiTouchInputBuilder.kt b/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/testing/MultiTouchInputBuilder.kt index 5fc00e50b8aa1..fe7ee0ec43078 100644 --- a/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/testing/MultiTouchInputBuilder.kt +++ b/ink/ink-authoring/src/androidMain/kotlin/androidx/ink/authoring/testing/MultiTouchInputBuilder.kt @@ -46,7 +46,7 @@ import androidx.annotation.VisibleForTesting * historical events preceding the primary [MotionEvent] data. */ @VisibleForTesting -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi public class MultiTouchInputBuilder( private val pointerCount: Int = 2, private val pointerId: IntArray = IntArray(pointerCount) { 9000 + it }, diff --git a/ink/ink-brush-compose/README.md b/ink/ink-brush-compose/README.md new file mode 100644 index 0000000000000..bb6b827eeedf5 --- /dev/null +++ b/ink/ink-brush-compose/README.md @@ -0,0 +1,5 @@ +# Ink Brush Compose Module + +Ink's `brush` module with extensions specific to Jetpack Compose. Separate from +the main `brush` module so that clients not using Compose can avoid the Compose +dependency. diff --git a/ink/ink-brush/README.md b/ink/ink-brush/README.md new file mode 100644 index 0000000000000..e5c68fa6a17dc --- /dev/null +++ b/ink/ink-brush/README.md @@ -0,0 +1,13 @@ +# Ink Brush Module + +This module contains logic for configuring the style of Ink's freehand strokes. +The precise style of a stroke is defined by `Brush`, which specifies a size, +color, epsilon (minimum unit considered visually distinguishable), and +`BrushFamily`. `BrushFamily` (analogous to font family) defines a general style +of stroke that can be in a variety of specific colors and sizes. The +experimental custom brush API brings the full flexibility of Ink's core +cross-platform implementation to Jetpack, allowing for configurable shapes, +textures, and dynamic behaviors. This lets Ink mimic strokes drawn by beautiful +and unique drawing tools (pencils, pens, markers, highlighters, brushes, and +more) but also far more unusual possibilities (washi tape, a laser pointer, +rainbows, a trail of clouds, the sky's the limit). diff --git a/ink/ink-brush/src/androidMain/kotlin/androidx/ink/brush/StockTextureBitmapStore.android.kt b/ink/ink-brush/src/androidMain/kotlin/androidx/ink/brush/StockTextureBitmapStore.android.kt index 8c76542405cf7..55ff7f1206452 100644 --- a/ink/ink-brush/src/androidMain/kotlin/androidx/ink/brush/StockTextureBitmapStore.android.kt +++ b/ink/ink-brush/src/androidMain/kotlin/androidx/ink/brush/StockTextureBitmapStore.android.kt @@ -18,7 +18,6 @@ package androidx.ink.brush import android.content.res.Resources import android.graphics.Bitmap -import android.graphics.BitmapFactory import androidx.annotation.RestrictTo /** @@ -28,7 +27,7 @@ import androidx.annotation.RestrictTo * give it access to the textures. */ // Not public until we're actually publishing stock brushes with stock textures. -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi public class StockTextureBitmapStore(private val resources: Resources) : TextureBitmapStore { private val idToBitmap = mutableMapOf() @@ -43,18 +42,7 @@ public class StockTextureBitmapStore(private val resources: Resources) : Texture * each textured brush family, as it needs to load and decode a bitmap from [resources]. To * prevent this, call [preloadStockBrushesTextures] in advance. */ - public override operator fun get(clientTextureId: String): Bitmap? = - idToBitmap[clientTextureId] - ?: when (clientTextureId) { - StockBrushes.pencilUnstableBackgroundTextureId -> R.drawable.pencil_background_v1 - else -> null - }?.let { resourceId -> - // computeIfAbsent is not available until API 24 (Android N). - checkNotNull(BitmapFactory.decodeResource(resources, resourceId)) { - "Failed to decode resource $resourceId for stock brush texture ID $clientTextureId" - } - .also { idToBitmap.put(clientTextureId, it) } - } + public override operator fun get(clientTextureId: String): Bitmap? = idToBitmap[clientTextureId] /** * Preloads the textures for the given [BrushFamily]. @@ -76,7 +64,8 @@ public class StockTextureBitmapStore(private val resources: Resources) : Texture } /** Whether the store contains a texture with the given client . */ - public fun contains(clientTextureId: String): Boolean = idToBitmap[clientTextureId] != null + public operator fun contains(clientTextureId: String): Boolean = + idToBitmap[clientTextureId] != null /** * Adds a texture to the store. diff --git a/ink/ink-brush/src/androidMain/res/drawable-nodpi/pencil_background_v1.png b/ink/ink-brush/src/androidMain/res/drawable-nodpi/pencil_background_v1.png deleted file mode 100644 index bbb435c782c08dfe20dc125f014e2a784ac69fd0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 188299 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ⅆ@R$^_T3o|GThS{n6iI7XUwO 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

62~V=h-uwr?-%O$e9A6C79Cu9Gdc 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 a68-N-fMe}Y&q)3N0000 = unmodifiableList(terminalNodes.toList()) + @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi + public val developerComment: String = BrushBehaviorNative.getDeveloperComment(nativePointer) + /** Constructs a [BrushBehavior] from a list of [TerminalNode]s. */ + @JvmOverloads public constructor( // The [terminalNodes] val above is a defensive copy of this parameter. - terminalNodes: List - ) : this(BrushBehaviorNative.createFromTerminalNodes(terminalNodes), terminalNodes) + terminalNodes: List, + developerComment: String = "", + ) : this( + BrushBehaviorNative.createFromTerminalNodes(terminalNodes, developerComment), + terminalNodes, + ) /** * Constructs a simple [BrushBehavior] using whatever [Node]s are necessary for the specified @@ -117,7 +125,7 @@ private constructor( if (responseTimeMillis != 0L) { node = DampingNode( - DampingSource.TIME_IN_SECONDS, + ProgressDomain.TIME_IN_SECONDS, responseTimeMillis.toFloat() / 1000.0f, node, ) @@ -154,43 +162,59 @@ private constructor( private var enabledToolTypes: Set = ALL_TOOL_TYPES private var isFallbackFor: OptionalInputProperty? = null + // These setters fall afoul of lint because there aren't corresponding fields on the + // BrushBehavior object. That's because this, like the brush behavior constructor that takes + // these parameters, provides a simplified interface over the more complicated constructor + // interface which takes a list of terminal nodes. + + @Suppress("MissingGetterMatchingBuilder") public fun setSource(source: Source): Builder = apply { this.source = source } + @Suppress("MissingGetterMatchingBuilder") public fun setTarget(target: Target): Builder = apply { this.target = target } + @Suppress("MissingGetterMatchingBuilder") public fun setSourceOutOfRangeBehavior(sourceOutOfRangeBehavior: OutOfRange): Builder = apply { this.sourceOutOfRangeBehavior = sourceOutOfRangeBehavior } + @Suppress("MissingGetterMatchingBuilder") public fun setSourceValueRangeStart(sourceValueRangeStart: Float): Builder = apply { this.sourceValueRangeStart = sourceValueRangeStart } + @Suppress("MissingGetterMatchingBuilder") public fun setSourceValueRangeEnd(sourceValueRangeEnd: Float): Builder = apply { this.sourceValueRangeEnd = sourceValueRangeEnd } + @Suppress("MissingGetterMatchingBuilder") public fun setTargetModifierRangeStart(targetModifierRangeStart: Float): Builder = apply { this.targetModifierRangeStart = targetModifierRangeStart } + @Suppress("MissingGetterMatchingBuilder") public fun setTargetModifierRangeEnd(targetModifierRangeEnd: Float): Builder = apply { this.targetModifierRangeEnd = targetModifierRangeEnd } + @Suppress("MissingGetterMatchingBuilder") public fun setResponseCurve(responseCurve: EasingFunction): Builder = apply { this.responseCurve = responseCurve } + @Suppress("MissingGetterMatchingBuilder") public fun setResponseTimeMillis(responseTimeMillis: Long): Builder = apply { this.responseTimeMillis = responseTimeMillis } + @Suppress("MissingGetterMatchingBuilder") public fun setEnabledToolTypes(enabledToolTypes: Set): Builder = apply { this.enabledToolTypes = enabledToolTypes.toSet() } + @Suppress("MissingGetterMatchingBuilder") public fun setIsFallbackFor(isFallbackFor: OptionalInputProperty?): Builder = apply { this.isFallbackFor = isFallbackFor } @@ -214,14 +238,17 @@ private constructor( override fun equals(other: Any?): Boolean { if (other == null || other !is BrushBehavior) return false if (other === this) return true - return terminalNodes == other.terminalNodes + return terminalNodes == other.terminalNodes && developerComment == other.developerComment } override fun hashCode(): Int { - return terminalNodes.hashCode() + var result = terminalNodes.hashCode() + result = 31 * result + developerComment.hashCode() + return result } - override fun toString(): String = "BrushBehavior($terminalNodes)" + override fun toString(): String = + "BrushBehavior($terminalNodes, developerComment=$developerComment)" /** Delete native BrushBehavior memory. */ // NOMUTANTS -- Not tested post garbage collection. @@ -847,45 +874,46 @@ private constructor( } } - /** Dimensions/units for measuring the [dampingGap] field of a [DampingNode] */ - public class DampingSource + /** Dimensions and units for measuring distance/time along the length/duration of a stroke. */ + public class ProgressDomain internal constructor(@JvmField internal val value: Int, private val name: String) { init { - check(value !in VALUE_TO_INSTANCE) { "Duplicate DampingSource value: $value." } + check(value !in VALUE_TO_INSTANCE) { "Duplicate ProgressDomain value: $value." } VALUE_TO_INSTANCE[value] = this } internal fun toSimpleString(): String = name - override fun toString(): String = "BrushBehavior.DampingSource.$name" + override fun toString(): String = "BrushBehavior.ProgressDomain.$name" public companion object { - private val VALUE_TO_INSTANCE = MutableIntObjectMap() + private val VALUE_TO_INSTANCE = MutableIntObjectMap() - internal fun fromInt(value: Int): DampingSource = - checkNotNull(VALUE_TO_INSTANCE.get(value)) { "Invalid DampingSource value: $value" } + internal fun fromInt(value: Int): ProgressDomain = + checkNotNull(VALUE_TO_INSTANCE.get(value)) { + "Invalid ProgressDomain value: $value" + } /** - * Value damping occurs over distance traveled by the input pointer, and the - * [dampingGap] is measured in centimeters. If the input data does not indicate the - * relationship between stroke units and physical units (e.g. as may be the case for - * programmatically-generated inputs), then no damping will be performed (i.e. the - * [dampingGap] will be treated as zero). + * Progress in input distance traveled since the start of the stroke, measured in + * centimeters. If the input data does not indicate the relationship between stroke + * units and physical units (e.g. as may be the case for programmatically-generated + * inputs), then special handling will be applied based on the node type. */ @JvmField - public val DISTANCE_IN_CENTIMETERS: DampingSource = - DampingSource(0, "DISTANCE_IN_CENTIMETERS") + public val DISTANCE_IN_CENTIMETERS: ProgressDomain = + ProgressDomain(0, "DISTANCE_IN_CENTIMETERS") /** - * Value damping occurs over distance traveled by the input pointer, and the - * [dampingGap] is measured in multiples of the brush size. + * Progress in input distance traveled since the start of the stroke, measured in + * multiples of the brush size. */ @JvmField - public val DISTANCE_IN_MULTIPLES_OF_BRUSH_SIZE: DampingSource = - DampingSource(1, "DISTANCE_IN_MULTIPLES_OF_BRUSH_SIZE") - /** Value damping occurs over time, and the [dampingGap] is measured in seconds. */ + public val DISTANCE_IN_MULTIPLES_OF_BRUSH_SIZE: ProgressDomain = + ProgressDomain(1, "DISTANCE_IN_MULTIPLES_OF_BRUSH_SIZE") + /** Progress in input time since the start of the stroke, measured in seconds. */ @JvmField - public val TIME_IN_SECONDS: DampingSource = DampingSource(2, "TIME_IN_SECONDS") + public val TIME_IN_SECONDS: ProgressDomain = ProgressDomain(2, "TIME_IN_SECONDS") } } @@ -928,6 +956,7 @@ private constructor( * their inputs must be chosen at construction time; therefore, they can only ever be assembled * into an acyclic graph. */ + @Suppress("NotCloseable") // Finalize is only used to free the native peer. public abstract class Node internal constructor( internal val nativePointer: Long, @@ -947,7 +976,7 @@ private constructor( } public companion object { - public fun wrapNative( + internal fun wrapNative( unownedNativePointer: Long, inputStack: ArrayDeque, ): Node = @@ -959,10 +988,11 @@ private constructor( 4 -> ToolTypeFilterNode.wrapNative(unownedNativePointer, inputStack) 5 -> DampingNode.wrapNative(unownedNativePointer, inputStack) 6 -> ResponseNode.wrapNative(unownedNativePointer, inputStack) - 7 -> BinaryOpNode.wrapNative(unownedNativePointer, inputStack) - 8 -> InterpolationNode.wrapNative(unownedNativePointer, inputStack) - 9 -> TargetNode.wrapNative(unownedNativePointer, inputStack) - 10 -> PolarTargetNode.wrapNative(unownedNativePointer, inputStack) + 7 -> IntegralNode.wrapNative(unownedNativePointer, inputStack) + 8 -> BinaryOpNode.wrapNative(unownedNativePointer, inputStack) + 9 -> InterpolationNode.wrapNative(unownedNativePointer, inputStack) + 10 -> TargetNode.wrapNative(unownedNativePointer, inputStack) + 11 -> PolarTargetNode.wrapNative(unownedNativePointer, inputStack) else -> throw IllegalArgumentException( "Unknown node type: ${BrushBehaviorNodeNative.getNodeType(unownedNativePointer)}" @@ -1080,7 +1110,7 @@ private constructor( */ public constructor( seed: Int, - varyOver: DampingSource, + varyOver: ProgressDomain, basePeriod: Float, ) : this(BrushBehaviorNodeNative.createNoise(seed, varyOver.value, basePeriod)) @@ -1092,7 +1122,8 @@ private constructor( public val seed: Int get() = BrushBehaviorNodeNative.getNoiseSeed(nativePointer) - public val varyOver: DampingSource = BrushBehaviorNodeNative.getNoiseVaryOver(nativePointer) + public val varyOver: ProgressDomain = + BrushBehaviorNodeNative.getNoiseVaryOver(nativePointer) public val basePeriod: Float get() = BrushBehaviorNodeNative.getNoiseBasePeriod(nativePointer) @@ -1249,7 +1280,7 @@ private constructor( * @param input input node that produces the value to be modified by the damping */ public constructor( - dampingSource: DampingSource, + dampingSource: ProgressDomain, dampingGap: Float, input: ValueNode, ) : this(BrushBehaviorNodeNative.createDamping(dampingSource.value, dampingGap), input) @@ -1261,7 +1292,7 @@ private constructor( ): DampingNode = DampingNode(unownedNativePointer, input = inputStack.removeLast()) } - public val dampingSource: DampingSource = + public val dampingSource: ProgressDomain = BrushBehaviorNodeNative.getDampingSource(nativePointer) public val dampingGap: Float @@ -1340,6 +1371,80 @@ private constructor( } } + /** A [ValueNode] that integrates an input value over time or distance. */ + public class IntegralNode + private constructor(nativePointer: Long, public val input: ValueNode) : + ValueNode(nativePointer, listOf(input)) { + + /** + * Creates an [IntegralNode] that integrates over an input value. + * + * @param integrateOver the metric to integrate the input over + * @param integralValueRangeStart the start of the range of values that the integral can + * produce + * @param integralValueRangeEnd the end of the range of values that the integral can produce + * @param integralOutOfRangeBehavior the behavior to use if the integral produces a value + * outside the specified range + * @param input input node that produces the value to be integrated + */ + public constructor( + integrateOver: ProgressDomain, + integralValueRangeStart: Float, + integralValueRangeEnd: Float, + integralOutOfRangeBehavior: OutOfRange, + input: ValueNode, + ) : this( + BrushBehaviorNodeNative.createIntegral( + integrateOver.value, + integralValueRangeStart, + integralValueRangeEnd, + integralOutOfRangeBehavior.value, + ), + input, + ) + + internal companion object { + internal fun wrapNative( + unownedNativePointer: Long, + inputStack: ArrayDeque, + ): IntegralNode = IntegralNode(unownedNativePointer, input = inputStack.removeLast()) + } + + public val integrateOver: ProgressDomain = + BrushBehaviorNodeNative.getIntegrateOver(nativePointer) + + public val integralValueRangeStart: Float + get() = BrushBehaviorNodeNative.getIntegralValueRangeStart(nativePointer) + + public val integralValueRangeEnd: Float + get() = BrushBehaviorNodeNative.getIntegralValueRangeEnd(nativePointer) + + public val integralOutOfRangeBehavior: OutOfRange = + BrushBehaviorNodeNative.getIntegralOutOfRangeBehavior(nativePointer) + + override fun toString(): String = + "IntegralNode(${integrateOver.toSimpleString()}, $integralValueRangeStart, $integralValueRangeEnd, ${integralOutOfRangeBehavior.toSimpleString()}, $input)" + + override fun equals(other: Any?): Boolean { + if (other == null || other !is IntegralNode) return false + if (other === this) return true + return integrateOver == other.integrateOver && + integralValueRangeStart == other.integralValueRangeStart && + integralValueRangeEnd == other.integralValueRangeEnd && + integralOutOfRangeBehavior == other.integralOutOfRangeBehavior && + input == other.input + } + + override fun hashCode(): Int { + var result = integrateOver.hashCode() + result = 31 * result + integralValueRangeStart.hashCode() + result = 31 * result + integralValueRangeEnd.hashCode() + result = 31 * result + integralOutOfRangeBehavior.hashCode() + result = 31 * result + input.hashCode() + return result + } + } + /** A [ValueNode] that combines two other values with a binary operation. */ public class BinaryOpNode private constructor( @@ -1655,7 +1760,10 @@ private object BrushBehaviorNative { NativeLoader.load() } - fun createFromTerminalNodes(terminalNodes: List): Long { + fun createFromTerminalNodes( + terminalNodes: List, + developerComment: String, + ): Long { val orderedNodes = ArrayDeque() val stack = ArrayDeque(terminalNodes) while (!stack.isEmpty()) { @@ -1664,11 +1772,18 @@ private object BrushBehaviorNative { stack.addAll(node.inputs) } } - return createFromOrderedNodes(orderedNodes.map { it.nativePointer }.toLongArray()) + return createFromOrderedNodes( + orderedNodes.map { it.nativePointer }.toLongArray(), + developerComment = developerComment, + ) } /** Creates a new native `BrushBehavior` with the given ordered nodes. */ - @UsedByNative external fun createFromOrderedNodes(orderdNodeNativePointers: LongArray): Long + @UsedByNative + external fun createFromOrderedNodes( + orderdNodeNativePointers: LongArray, + developerComment: String, + ): Long /** Release the underlying memory allocated in [createFromOrderedNodes]. */ @UsedByNative external fun free(nativePointer: Long) @@ -1676,6 +1791,8 @@ private object BrushBehaviorNative { /** Returns the number of `BrushBehavior::Node`s in the native `BrushBehavior`. */ @UsedByNative external fun getNodeCount(nativePointer: Long): Int + @UsedByNative external fun getDeveloperComment(nativePointer: Long): String + /** * Returns an unowned native pointer to a new, stack-allocated copy of the native * `BrushBehavior::Node` at the given index in the pointed-at `BrushBehavior`. @@ -1723,6 +1840,14 @@ private object BrushBehaviorNodeNative { @UsedByNative external fun createResponse(easingFunctionNativePointer: Long): Long + @UsedByNative + external fun createIntegral( + integrateOver: Int, + integralValueRangeStart: Float, + integralValueRangeEnd: Float, + integralOutOfRangeBehavior: Int, + ): Long + @UsedByNative external fun createBinaryOp(operation: Int): Long @UsedByNative external fun createInterpolation(interpolation: Int): Long @@ -1771,8 +1896,8 @@ private object BrushBehaviorNodeNative { @UsedByNative external fun getNoiseSeed(nativePointer: Long): Int - fun getNoiseVaryOver(nativePointer: Long): BrushBehavior.DampingSource = - BrushBehavior.DampingSource.fromInt(getNoiseVaryOverInt(nativePointer)) + fun getNoiseVaryOver(nativePointer: Long): BrushBehavior.ProgressDomain = + BrushBehavior.ProgressDomain.fromInt(getNoiseVaryOverInt(nativePointer)) @UsedByNative private external fun getNoiseVaryOverInt(nativePointer: Long): Int @@ -1799,17 +1924,33 @@ private object BrushBehaviorNodeNative { // DampingNode accessors: - fun getDampingSource(nativePointer: Long): BrushBehavior.DampingSource = - BrushBehavior.DampingSource.fromInt(getDampingSourceInt(nativePointer)) + fun getDampingSource(nativePointer: Long): BrushBehavior.ProgressDomain = + BrushBehavior.ProgressDomain.fromInt(getDampingSourceInt(nativePointer)) @UsedByNative private external fun getDampingSourceInt(nativePointer: Long): Int @UsedByNative external fun getDampingGap(nativePointer: Long): Float - // Getters for ResponseNode: + // ResponseNode accessors: @UsedByNative external fun newCopyOfResponseEasingFunction(nativePointer: Long): Long + // IntegralNode accessors: + + fun getIntegrateOver(nativePointer: Long): BrushBehavior.ProgressDomain = + BrushBehavior.ProgressDomain.fromInt(getIntegrateOverInt(nativePointer)) + + @UsedByNative private external fun getIntegrateOverInt(nativePointer: Long): Int + + @UsedByNative external fun getIntegralValueRangeStart(nativePointer: Long): Float + + @UsedByNative external fun getIntegralValueRangeEnd(nativePointer: Long): Float + + fun getIntegralOutOfRangeBehavior(nativePointer: Long): BrushBehavior.OutOfRange = + BrushBehavior.OutOfRange.fromInt(getIntegralOutOfRangeBehaviorInt(nativePointer)) + + @UsedByNative private external fun getIntegralOutOfRangeBehaviorInt(nativePointer: Long): Int + // BinaryOpNode accessors: fun getBinaryOperation(nativePointer: Long): BrushBehavior.BinaryOp = diff --git a/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/BrushCoat.kt b/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/BrushCoat.kt index 64a0f4f9426ad..38459e79b8217 100644 --- a/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/BrushCoat.kt +++ b/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/BrushCoat.kt @@ -35,7 +35,7 @@ import kotlin.jvm.JvmStatic * crosses over itself, as though each coat were painted in its entirety one at a time. */ @ExperimentalInkCustomBrushApi -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi @Suppress("NotCloseable") // Finalize is only used to free the native peer. public class BrushCoat private constructor( @@ -87,8 +87,14 @@ private constructor( * @param tip The tip used to apply the paint. * @param paint The paint to be applied for this coat. */ - @JvmOverloads - public constructor(tip: BrushTip = BrushTip(), paint: BrushPaint) : this(tip, listOf(paint)) + public constructor(tip: BrushTip, paint: BrushPaint) : this(tip, listOf(paint)) + + /** + * Creates a [BrushCoat] with the given [paint] and the default [BrushTip]. + * + * @param paint The paint to be applied for this coat. + */ + public constructor(paint: BrushPaint) : this(BrushTip(), listOf(paint)) /** * Creates a copy of `this` and allows named properties to be altered while keeping the rest diff --git a/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/BrushFamily.kt b/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/BrushFamily.kt index 05571ddc928d6..9c5c2722c5727 100644 --- a/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/BrushFamily.kt +++ b/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/BrushFamily.kt @@ -42,19 +42,22 @@ private constructor( @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public val nativePointer: Long, coats: List, /** The [InputModel] that will be used by a [Brush] in this [BrushFamily]. */ - @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi + @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi public val inputModel: InputModel, ) { /** The [BrushCoat]s that make up this [BrushFamily]. */ - @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi + @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi public val coats: List = unmodifiableList(coats.toList()) /** Client-provided identifier for this [BrushFamily]. */ // Cached to avoid converting C++ string to JVM string every time. - @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi + @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi public val clientBrushFamilyId: String = BrushFamilyNative.getClientBrushFamilyId(nativePointer) + @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi + public val developerComment: String = BrushFamilyNative.getDeveloperComment(nativePointer) + /** * Creates a [BrushFamily] with the given [BrushCoat]s. * @@ -62,53 +65,69 @@ private constructor( * @param clientBrushFamilyId Optional-provided identifier for this [BrushFamily]. * @param inputModel The [InputModel] that will be used by a [Brush] in this [BrushFamily]. */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi @ExperimentalInkCustomBrushApi @JvmOverloads public constructor( coats: List, - clientBrushFamilyId: String = "", inputModel: InputModel = DEFAULT_INPUT_MODEL, + clientBrushFamilyId: String = "", + developerComment: String = "", ) : this( - BrushFamilyNative.create( - coats.map { it.nativePointer }.toLongArray(), - clientBrushFamilyId, - inputModel.nativePointer, - ), - coats, - inputModel, + nativePointer = + BrushFamilyNative.create( + coatNativePointers = coats.map { it.nativePointer }.toLongArray(), + inputModelPointer = inputModel.nativePointer, + clientBrushFamilyId = clientBrushFamilyId, + developerComment = developerComment, + ), + coats = coats, + inputModel = inputModel, ) - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi @ExperimentalInkCustomBrushApi @JvmOverloads public constructor( tip: BrushTip = BrushTip(), paint: BrushPaint = BrushPaint(), - clientBrushFamilyId: String = "", inputModel: InputModel = DEFAULT_INPUT_MODEL, - ) : this(listOf(BrushCoat(tip, paint)), clientBrushFamilyId, inputModel) + clientBrushFamilyId: String = "", + developerComment: String = "", + ) : this( + coats = listOf(BrushCoat(tip, paint)), + inputModel = inputModel, + clientBrushFamilyId = clientBrushFamilyId, + developerComment = developerComment, + ) /** * Creates a copy of `this` and allows named properties to be altered while keeping the rest * unchanged. */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi @ExperimentalInkCustomBrushApi @JvmSynthetic public fun copy( coats: List = this.coats, - clientBrushFamilyId: String = this.clientBrushFamilyId, inputModel: InputModel = this.inputModel, + clientBrushFamilyId: String = this.clientBrushFamilyId, + developerComment: String = this.developerComment, ): BrushFamily { return if ( coats == this.coats && + inputModel == this.inputModel && clientBrushFamilyId == this.clientBrushFamilyId && - inputModel == this.inputModel + developerComment == this.developerComment ) { this } else { - BrushFamily(coats, clientBrushFamilyId, inputModel) + BrushFamily( + coats = coats, + inputModel = inputModel, + clientBrushFamilyId = clientBrushFamilyId, + developerComment = developerComment, + ) } } @@ -116,18 +135,20 @@ private constructor( * Creates a copy of `this` and allows named properties to be altered while keeping the rest * unchanged. */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi @ExperimentalInkCustomBrushApi @JvmSynthetic public fun copy( coat: BrushCoat, - clientBrushFamilyId: String = this.clientBrushFamilyId, inputModel: InputModel = this.inputModel, + clientBrushFamilyId: String = this.clientBrushFamilyId, + developerComment: String = this.developerComment, ): BrushFamily { return copy( coats = listOf(coat), - clientBrushFamilyId = clientBrushFamilyId, inputModel = inputModel, + clientBrushFamilyId = clientBrushFamilyId, + developerComment = developerComment, ) } @@ -135,19 +156,21 @@ private constructor( * Creates a copy of `this` and allows named properties to be altered while keeping the rest * unchanged. */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi @ExperimentalInkCustomBrushApi @JvmSynthetic public fun copy( tip: BrushTip, paint: BrushPaint, - clientBrushFamilyId: String = this.clientBrushFamilyId, inputModel: InputModel = this.inputModel, + clientBrushFamilyId: String = this.clientBrushFamilyId, + developerComment: String = this.developerComment, ): BrushFamily { return copy( coat = BrushCoat(tip, paint), - clientBrushFamilyId = clientBrushFamilyId, inputModel = inputModel, + clientBrushFamilyId = clientBrushFamilyId, + developerComment = developerComment, ) } @@ -155,13 +178,14 @@ private constructor( * Returns a [Builder] with values set equivalent to `this`. Java developers, use the returned * builder to build a copy of a BrushFamily. */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi @ExperimentalInkCustomBrushApi public fun toBuilder(): Builder = Builder() .setCoats(coats) - .setClientBrushFamilyId(clientBrushFamilyId) .setInputModel(inputModel) + .setClientBrushFamilyId(clientBrushFamilyId) + .setDeveloperComment(developerComment) /** * Builder for [BrushFamily]. @@ -170,16 +194,15 @@ private constructor( * overriding only as needed. For example: `BrushFamily family = new * BrushFamily.Builder().coat(presetBrushCoat).build();` */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi @ExperimentalInkCustomBrushApi public class Builder { private var coats: List = listOf(BrushCoat(BrushTip(), BrushPaint())) - private var clientBrushFamilyId: String = "" private var inputModel: InputModel = DEFAULT_INPUT_MODEL + private var clientBrushFamilyId: String = "" + private var developerComment: String = "" - public fun setCoat(tip: BrushTip, paint: BrushPaint): Builder = - setCoat(BrushCoat(tip, paint)) - + @Suppress("MissingGetterMatchingBuilder") public fun setCoat(coat: BrushCoat): Builder = setCoats(listOf(coat)) public fun setCoats(coats: List): Builder { @@ -187,17 +210,28 @@ private constructor( return this } + public fun setInputModel(inputModel: InputModel): Builder { + this.inputModel = inputModel + return this + } + public fun setClientBrushFamilyId(clientBrushFamilyId: String): Builder { this.clientBrushFamilyId = clientBrushFamilyId return this } - public fun setInputModel(inputModel: InputModel): Builder { - this.inputModel = inputModel + public fun setDeveloperComment(developerComment: String): Builder { + this.developerComment = developerComment return this } - public fun build(): BrushFamily = BrushFamily(coats, clientBrushFamilyId, inputModel) + public fun build(): BrushFamily = + BrushFamily( + coats = coats, + inputModel = inputModel, + clientBrushFamilyId = clientBrushFamilyId, + developerComment = developerComment, + ) } override fun equals(other: Any?): Boolean { @@ -205,19 +239,21 @@ private constructor( // NOMUTANTS -- Check the instance first to short circuit faster. if (other === this) return true return coats == other.coats && + inputModel == other.inputModel && clientBrushFamilyId == other.clientBrushFamilyId && - inputModel == other.inputModel + developerComment == other.developerComment } override fun hashCode(): Int { var result = coats.hashCode() - result = 31 * result + clientBrushFamilyId.hashCode() result = 31 * result + inputModel.hashCode() + result = 31 * result + clientBrushFamilyId.hashCode() + result = 31 * result + developerComment.hashCode() return result } override fun toString(): String = - "BrushFamily(coats=$coats, clientBrushFamilyId=$clientBrushFamilyId, inputModel=$inputModel)" + "BrushFamily(developerComment=$developerComment, coats=$coats, inputModel=$inputModel, clientBrushFamilyId=$clientBrushFamilyId)" /** Deletes native BrushFamily memory. */ // NOMUTANTS -- Not tested post garbage collection. @@ -253,12 +289,12 @@ private constructor( ) /** Returns a new [BrushFamily.Builder]. */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi @ExperimentalInkCustomBrushApi @JvmStatic public fun builder(): Builder = Builder() - /** The recommended spring-based input modeler. */ + /** The old spring-based input modeler. */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi @ExperimentalInkCustomBrushApi @JvmField @@ -274,7 +310,7 @@ private constructor( public val EXPERIMENTAL_NAIVE_MODEL: InputModel = NoParametersModel.EXPERIMENTAL_NAIVE_MODEL /** The default [InputModel] that will be used by a [BrushFamily] when none is specified. */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi @ExperimentalInkCustomBrushApi @JvmField public val DEFAULT_INPUT_MODEL: InputModel = SlidingWindowModel() @@ -286,8 +322,9 @@ private constructor( * to be noisy, and must be smoothed before being passed into a brush's behaviors and extruded * into a mesh in order to get a good-looking stroke. */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi @ExperimentalInkCustomBrushApi + @Suppress("NotCloseable") // Finalize is only used to free the native peer. public abstract class InputModel internal constructor(internal val nativePointer: Long) { // NOMUTANTS -- Not tested post garbage collection. protected fun finalize() { @@ -337,7 +374,7 @@ private constructor( } } - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi @ExperimentalInkCustomBrushApi public class SlidingWindowModel internal constructor(nativePointer: Long) : InputModel(nativePointer) { @@ -387,16 +424,19 @@ private object BrushFamilyNative { @UsedByNative external fun create( coatNativePointers: LongArray, - clientBrushFamilyId: String, inputModelPointer: Long, + clientBrushFamilyId: String, + developerComment: String, ): Long /** Release the underlying memory allocated in [create]. */ @UsedByNative external fun free(nativePointer: Long) + @UsedByNative external fun getBrushCoatCount(nativePointer: Long): Int + @UsedByNative external fun getClientBrushFamilyId(nativePointer: Long): String - @UsedByNative external fun getBrushCoatCount(nativePointer: Long): Int + @UsedByNative external fun getDeveloperComment(nativePointer: Long): String /** * Returns a new, unowned native pointer to a copy of the `BrushCoat` at index for the diff --git a/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/BrushPaint.kt b/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/BrushPaint.kt index 427f4a9076bbd..0b178d68462f7 100644 --- a/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/BrushPaint.kt +++ b/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/BrushPaint.kt @@ -42,7 +42,7 @@ import kotlin.jvm.JvmSynthetic * - The final combined texture (source) is blended with the (possibly adjusted per-vertex) brush * color (destination) according to the blend mode of the last texture layer. */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi @ExperimentalInkCustomBrushApi @Suppress("NotCloseable") // Finalize is only used to free the native peer. public class BrushPaint @@ -86,6 +86,7 @@ private constructor( ) /** Uses this paint's color functions (if any) to transform the given brush color. */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public fun applyColorFunctions(color: ComposeColor): ComposeColor { var transformedColor = color for (colorFunction in colorFunctions) { diff --git a/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/BrushTip.kt b/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/BrushTip.kt index 1cd856ca31b89..ced97672d168e 100644 --- a/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/BrushTip.kt +++ b/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/BrushTip.kt @@ -40,7 +40,7 @@ import kotlin.jvm.JvmSynthetic * be used to augment the [Brush] color when drawing. The default values below produce a static * circular tip shape with diameter equal to the [Brush] size and no color shift. */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi @ExperimentalInkCustomBrushApi @Suppress("NotCloseable") // Finalize is only used to free the native peer. public class BrushTip diff --git a/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/ColorExtensions.kt b/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/ColorExtensions.kt index 1d0124ff1d5ab..71f5bfc6aab28 100644 --- a/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/ColorExtensions.kt +++ b/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/ColorExtensions.kt @@ -20,7 +20,8 @@ import androidx.annotation.RestrictTo import androidx.ink.brush.color.Color as ComposeColor import androidx.ink.brush.color.colorspace.ColorSpace as ComposeColorSpace import androidx.ink.brush.color.colorspace.ColorSpaces as ComposeColorSpaces -import androidx.ink.brush.color.colorspace.Rgb as ComposeRgbColorSpace +import androidx.ink.nativeloader.NativeLoader +import androidx.ink.nativeloader.UsedByNative @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public fun ComposeColor.toColorInInkSupportedColorSpace(): ComposeColor { @@ -38,7 +39,7 @@ internal fun ComposeColorSpace.toInkColorSpaceId() = else -> throw IllegalArgumentException("Unsupported Compose color space") } -internal fun composeColorSpaceFromInkColorSpaceId(id: Int): ComposeRgbColorSpace = +internal fun composeColorSpaceFromInkColorSpaceId(id: Int) = when (id) { 0 -> ComposeColorSpaces.Srgb 1 -> ComposeColorSpaces.DisplayP3 @@ -47,3 +48,33 @@ internal fun composeColorSpaceFromInkColorSpaceId(id: Int): ComposeRgbColorSpace internal fun ComposeColorSpace.isSupportedInInk() = (this == ComposeColorSpaces.Srgb || this == ComposeColorSpaces.DisplayP3) + +@UsedByNative +internal object ColorNative { + init { + NativeLoader.load() + } + + /** + * This is a callback used by BrushNative.computeComposeColorLong and + * ColorFunctionNative.computeReplaceColorLong. + */ + @UsedByNative + @JvmStatic + fun composeColorLongFromComponents( + colorSpaceId: Int, + redGammaCorrected: Float, + greenGammaCorrected: Float, + blueGammaCorrected: Float, + alpha: Float, + ): Long = + ComposeColor( + redGammaCorrected, + greenGammaCorrected, + blueGammaCorrected, + alpha, + colorSpace = composeColorSpaceFromInkColorSpaceId(colorSpaceId), + ) + .value + .toLong() +} diff --git a/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/ColorFunction.kt b/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/ColorFunction.kt index db1b230bb8bdf..f376a98df6592 100644 --- a/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/ColorFunction.kt +++ b/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/ColorFunction.kt @@ -27,13 +27,14 @@ import androidx.ink.nativeloader.UsedByNative /** A [ColorFunction] defines a mapping over colors. */ @ExperimentalInkCustomBrushApi -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi // NotCloseable: Finalize is only used to free the native peer. @Suppress("NotCloseable") public abstract class ColorFunction private constructor(internal val nativePointer: Long) { /** Transforms the input color into a new color. */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public abstract fun transformComposeColor(color: ComposeColor): ComposeColor /** Transforms the input color into a new color. */ @@ -74,6 +75,7 @@ public abstract class ColorFunction private constructor(internal val nativePoint public val multiplier: Float get() = ColorFunctionNative.getOpacityMultiplier(nativePointer) + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) override fun transformComposeColor(color: ComposeColor): ComposeColor = color.copy(alpha = color.alpha * multiplier) @@ -112,6 +114,7 @@ public abstract class ColorFunction private constructor(internal val nativePoint public val colorIntArgb: Int @ColorInt get(): Int = internalColor.toArgb() + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) override fun transformComposeColor(color: ComposeColor): ComposeColor = this.internalColor override fun equals(other: Any?): Boolean { @@ -128,6 +131,7 @@ public abstract class ColorFunction private constructor(internal val nativePoint public companion object { @JvmStatic + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public fun withComposeColor(color: ComposeColor): ReplaceColor = ReplaceColor( color.toColorInInkSupportedColorSpace().let { convertedColor -> diff --git a/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/EasingFunction.kt b/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/EasingFunction.kt index b53e2f7561a24..0b59583096d67 100644 --- a/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/EasingFunction.kt +++ b/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/EasingFunction.kt @@ -31,7 +31,7 @@ import kotlin.jvm.JvmField * values outside [0, 1] are possible. */ @ExperimentalInkCustomBrushApi -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi // NotCloseable: Finalize is only used to free the native peer. @Suppress("NotCloseable") diff --git a/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/StockBrushes.kt b/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/StockBrushes.kt index a50592f070afd..71ae5ddd9a71c 100644 --- a/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/StockBrushes.kt +++ b/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/StockBrushes.kt @@ -17,22 +17,8 @@ package androidx.ink.brush import androidx.annotation.RestrictTo -import androidx.ink.brush.BrushBehavior.BinaryOp -import androidx.ink.brush.BrushBehavior.BinaryOpNode -import androidx.ink.brush.BrushBehavior.OutOfRange -import androidx.ink.brush.BrushBehavior.ResponseNode -import androidx.ink.brush.BrushBehavior.Source -import androidx.ink.brush.BrushBehavior.SourceNode -import androidx.ink.brush.BrushBehavior.Target -import androidx.ink.brush.BrushBehavior.TargetNode -import androidx.ink.brush.BrushPaint.BlendMode -import androidx.ink.brush.BrushPaint.TextureLayer -import androidx.ink.brush.BrushPaint.TextureMapping -import androidx.ink.brush.BrushPaint.TextureOrigin -import androidx.ink.brush.BrushPaint.TextureSizeUnit -import androidx.ink.brush.BrushPaint.TextureWrap -import androidx.ink.geometry.Angle -import androidx.ink.geometry.AngleDegreesFloat +import androidx.ink.nativeloader.NativeLoader +import androidx.ink.nativeloader.UsedByNative import kotlin.jvm.JvmStatic /** @@ -62,61 +48,20 @@ import kotlin.jvm.JvmStatic @OptIn(ExperimentalInkCustomBrushApi::class) public object StockBrushes { - /** - * The scale factor to apply to both X and Y dimensions of the mini emoji brush tip and texture - * layer size. - */ - private const val EMOJI_STAMP_SCALE = 1.5f - - private val STOCK_INPUT_MODEL: BrushFamily.InputModel = BrushFamily.SlidingWindowModel() - @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi @JvmStatic public val predictionFadeOutBehavior: BrushBehavior by lazy { - BrushBehavior( - terminalNodes = - listOf( - TargetNode( - target = Target.OPACITY_MULTIPLIER, - targetModifierRangeStart = 1F, - targetModifierRangeEnd = 0.3F, - BinaryOpNode( - operation = BinaryOp.PRODUCT, - firstInput = - SourceNode( - source = Source.PREDICTED_TIME_ELAPSED_IN_MILLIS, - sourceValueRangeStart = 0F, - sourceValueRangeEnd = 24F, - ), - // The second branch of the binary op node keeps the opacity fade-out - // from starting - // until the predicted inputs have traveled at least 1.5x brush-size. - secondInput = - ResponseNode( - responseCurve = EasingFunction.Predefined.EASE_IN_OUT, - input = - SourceNode( - source = - Source - .PREDICTED_DISTANCE_TRAVELED_IN_MULTIPLES_OF_BRUSH_SIZE, - sourceValueRangeStart = 1.5F, - sourceValueRangeEnd = 2F, - ), - ), - ), - ) - ) - ) + BrushBehavior.wrapNative(StockBrushesNative.predictionFadeOutBehavior()) } /** Version option for the [marker] stock brush factory function. */ - public class MarkerVersion private constructor(private val name: String) { + public class MarkerVersion private constructor(internal val value: Int) { - override fun toString(): String = "MarkerVersion.$name" + override fun toString(): String = "MarkerVersion.V$value" public companion object { /** Initial version of a simple, circular fixed-width brush. */ - @JvmField public val V1: MarkerVersion = MarkerVersion("V1") + @JvmField public val V1: MarkerVersion = MarkerVersion(1) /** Whichever version of marker is currently the latest. */ @JvmField public val LATEST: MarkerVersion = V1 @@ -124,10 +69,7 @@ public object StockBrushes { } private val markerV1 by lazy { - BrushFamily( - tip = BrushTip(behaviors = listOf(predictionFadeOutBehavior)), - inputModel = STOCK_INPUT_MODEL, - ) + BrushFamily.wrapNative(StockBrushesNative.marker(MarkerVersion.V1.value)) } /** @@ -144,16 +86,16 @@ public object StockBrushes { } /** Version option for the [pressurePen] stock brush factory function. */ - public class PressurePenVersion private constructor(private val name: String) { + public class PressurePenVersion private constructor(internal val value: Int) { - override fun toString(): String = "PressurePenVersion.$name" + override fun toString(): String = "PressurePenVersion.V$value" public companion object { /** * Initial version of a pressure- and speed-sensitive brush that is optimized for * handwriting with a stylus. */ - @JvmField public val V1: PressurePenVersion = PressurePenVersion("V1") + @JvmField public val V1: PressurePenVersion = PressurePenVersion(1) /** * The latest version of a pressure- and speed-sensitive brush that is optimized for @@ -164,56 +106,7 @@ public object StockBrushes { } private val pressurePenV1 by lazy { - BrushFamily( - tip = - BrushTip( - behaviors = - listOf( - predictionFadeOutBehavior, - BrushBehavior( - Source.DISTANCE_REMAINING_IN_MULTIPLES_OF_BRUSH_SIZE, - Target.SIZE_MULTIPLIER, - sourceValueRangeStart = 3f, - sourceValueRangeEnd = 0f, - targetModifierRangeStart = 1f, - targetModifierRangeEnd = 0.75f, - OutOfRange.CLAMP, - ), - BrushBehavior( - Source.NORMALIZED_DIRECTION_Y, - Target.SIZE_MULTIPLIER, - sourceValueRangeStart = 0.45f, - sourceValueRangeEnd = 0.65f, - targetModifierRangeStart = 1.0f, - targetModifierRangeEnd = 1.17f, - OutOfRange.CLAMP, - responseTimeMillis = 25L, - ), - BrushBehavior( - Source.INPUT_ACCELERATION_LATERAL_IN_CENTIMETERS_PER_SECOND_SQUARED, - Target.SIZE_MULTIPLIER, - sourceValueRangeStart = -80f, - sourceValueRangeEnd = -230f, - targetModifierRangeStart = 1.0f, - targetModifierRangeEnd = 1.25f, - OutOfRange.CLAMP, - responseTimeMillis = 25L, - ), - BrushBehavior( - Source.NORMALIZED_PRESSURE, - Target.SIZE_MULTIPLIER, - sourceValueRangeStart = 0.8f, - sourceValueRangeEnd = 1f, - targetModifierRangeStart = 1.0f, - targetModifierRangeEnd = 1.5f, - OutOfRange.CLAMP, - responseTimeMillis = 30L, - enabledToolTypes = setOf(InputToolType.STYLUS), - ), - ) - ), - inputModel = STOCK_INPUT_MODEL, - ) + BrushFamily.wrapNative(StockBrushesNative.pressurePen(PressurePenVersion.V1.value)) } /** @@ -232,16 +125,16 @@ public object StockBrushes { } /** Version option for the [highlighter] stock brush factory function. */ - public class HighlighterVersion private constructor(private val name: String) { + public class HighlighterVersion private constructor(internal val value: Int) { - override fun toString(): String = "HighlighterVersion.$name" + override fun toString(): String = "HighlighterVersion.V$value" public companion object { /** * Initial of a chisel-tip brush that is intended for highlighting text in a document * (when used with a translucent brush color). */ - @JvmField public val V1: HighlighterVersion = HighlighterVersion("V1") + @JvmField public val V1: HighlighterVersion = HighlighterVersion(1) /** * The latest version of a chisel-tip brush that is intended for highlighting text in a @@ -253,66 +146,13 @@ public object StockBrushes { private val selfOverlapToHighlighterV1 = listOf(SelfOverlap.ANY, SelfOverlap.ACCUMULATE, SelfOverlap.DISCARD).associateWith { - lazy { highlighterV1(it) } + lazy { + BrushFamily.wrapNative( + StockBrushesNative.highlighter(it.value, HighlighterVersion.V1.value) + ) + } } - private fun highlighterV1(selfOverlap: SelfOverlap) = - BrushFamily( - tip = - BrushTip( - scaleX = 0.25f, - scaleY = 1f, - cornerRounding = 0.3f, - rotationDegrees = 150f, - behaviors = - listOf( - predictionFadeOutBehavior, - BrushBehavior( - Source.DISTANCE_REMAINING_IN_MULTIPLES_OF_BRUSH_SIZE, - Target.CORNER_ROUNDING_OFFSET, - sourceValueRangeStart = 0f, - sourceValueRangeEnd = 1f, - targetModifierRangeStart = 0.3f, - targetModifierRangeEnd = 1f, - OutOfRange.CLAMP, - responseTimeMillis = 15L, - ), - BrushBehavior( - Source.DISTANCE_TRAVELED_IN_MULTIPLES_OF_BRUSH_SIZE, - Target.CORNER_ROUNDING_OFFSET, - sourceValueRangeStart = 0f, - sourceValueRangeEnd = 1f, - targetModifierRangeStart = 0.3f, - targetModifierRangeEnd = 1f, - OutOfRange.CLAMP, - responseTimeMillis = 15L, - ), - BrushBehavior( - Source.DISTANCE_TRAVELED_IN_MULTIPLES_OF_BRUSH_SIZE, - Target.OPACITY_MULTIPLIER, - sourceValueRangeStart = 0f, - sourceValueRangeEnd = 3f, - targetModifierRangeStart = 1.1f, - targetModifierRangeEnd = 1f, - OutOfRange.CLAMP, - responseTimeMillis = 15L, - ), - BrushBehavior( - Source.DISTANCE_REMAINING_IN_MULTIPLES_OF_BRUSH_SIZE, - Target.OPACITY_MULTIPLIER, - sourceValueRangeStart = 0f, - sourceValueRangeEnd = 3f, - targetModifierRangeStart = 1.1f, - targetModifierRangeEnd = 1f, - OutOfRange.CLAMP, - responseTimeMillis = 15L, - ), - ), - ), - paint = BrushPaint(selfOverlap = selfOverlap), - inputModel = STOCK_INPUT_MODEL, - ) - /** * Factory function for constructing a chisel-tip brush that is intended for highlighting text * in a document (when used with a translucent brush color). @@ -342,9 +182,9 @@ public object StockBrushes { } /** Version option for the [dashedLine] stock brush factory function. */ - public class DashedLineVersion private constructor(private val name: String) { + public class DashedLineVersion private constructor(internal val value: Int) { - override fun toString(): String = "DashedLineVersion.$name" + override fun toString(): String = "DashedLineVersion.V$value" public companion object { /** @@ -352,7 +192,7 @@ public object StockBrushes { * them. This may be decorative, or can be used to signify a user interaction like * free-form (lasso) selection. */ - @JvmField public val V1: DashedLineVersion = DashedLineVersion("V1") + @JvmField public val V1: DashedLineVersion = DashedLineVersion(1) /** The latest version of a dashed-line brush. */ @JvmField public val LATEST: DashedLineVersion = V1 @@ -360,35 +200,7 @@ public object StockBrushes { } private val dashedLineV1 by lazy { - BrushFamily( - tip = - BrushTip( - scaleX = 2F, - scaleY = 1F, - cornerRounding = 0.45F, - particleGapDistanceScale = 3F, - behaviors = - listOf( - predictionFadeOutBehavior, - BrushBehavior( - listOf( - TargetNode( - Target.ROTATION_OFFSET_IN_RADIANS, - -Angle.HALF_TURN_RADIANS, - Angle.HALF_TURN_RADIANS, - SourceNode( - Source.DIRECTION_ABOUT_ZERO_IN_RADIANS, - -Angle.HALF_TURN_RADIANS, - Angle.HALF_TURN_RADIANS, - OutOfRange.CLAMP, - ), - ) - ) - ), - ), - ), - inputModel = STOCK_INPUT_MODEL, - ) + BrushFamily.wrapNative(StockBrushesNative.dashedLine(DashedLineVersion.V1.value)) } /** @@ -407,51 +219,10 @@ public object StockBrushes { else -> throw IllegalArgumentException("Unsupported dashed line version: $version") } - /** The client texture ID for the background of the version-1 pencil brush. */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi - @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi - @JvmStatic - public val pencilUnstableBackgroundTextureId: String = - "androidx.ink.brush.StockBrushes.pencil_background_unstable" - - /** - * A development version of a brush that looks like pencil marks on subtly textured paper. - * - * In order to use this brush, the [TextureBitmapStore] provided to your renderer must map the - * [pencilUnstableBackgroundTextureId] to a bitmap; otherwise, no texture will be visible. - * Android callers may want to use [StockTextureBitmapStore] to provide this mapping. - * - * The behavior of this [BrushFamily] may change significantly in future releases. Once it has - * stabilized, it will be renamed to `pencilV1`. - */ - // TODO: b/373587591 - Change this to be consistent with the other brush factory functions - // before - // release. - @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi - @JvmStatic - public val pencilUnstable: BrushFamily by lazy { - BrushFamily( - tip = BrushTip(behaviors = listOf(predictionFadeOutBehavior)), - paint = - BrushPaint( - listOf( - TextureLayer( - clientTextureId = pencilUnstableBackgroundTextureId, - sizeX = 512F, - sizeY = 512F, - sizeUnit = TextureSizeUnit.STROKE_COORDINATES, - mapping = TextureMapping.TILING, - ) - ) - ), - inputModel = STOCK_INPUT_MODEL, - ) - } - /** Version option for the [emojiHighlighter] stock brush factory function. */ - public class EmojiHighlighterVersion private constructor(private val name: String) { + public class EmojiHighlighterVersion private constructor(internal val value: Int) { - override fun toString(): String = "EmojiHighlighterVersion.$name" + override fun toString(): String = "EmojiHighlighterVersion.V$value" public companion object { /** @@ -459,379 +230,13 @@ public object StockBrushes { * moving emoji sticker, possibly with a trail of miniature versions of the chosen emoji * sparkling behind. */ - @JvmField public val V1: EmojiHighlighterVersion = EmojiHighlighterVersion("V1") + @JvmField public val V1: EmojiHighlighterVersion = EmojiHighlighterVersion(1) /** Whichever version of emoji highlighter is currently the latest. */ @JvmField public val LATEST: EmojiHighlighterVersion = V1 } } - /** - * A brush coat that looks like a mini emoji. - * - * @param clientTextureId the client texture ID of the emoji to appear in the coat. - * @param tipScale the scale factor to apply to both X and Y dimensions of the mini emoji - * @param tipRotationDegrees the rotation to apply to the mini emoji - * @param tipParticleGapDistanceScale the scale factor to apply to the particle gap distance - * @param positionOffsetRangeStart the start of the range for the position offset behavior - * @param positionOffsetRangeEnd the end of the range for the position offset behavior - * @param distanceTraveledRangeStart the start of the range for the distance traveled behavior - * @param distanceTraveledRangeEnd the end of the range for the distance traveled behavior - * @param luminosityRangeStart the start of the range for the luminosity behavior - * @param luminosityRangeEnd the end of the range for the luminosity behavior - */ - private fun miniEmojiCoat( - clientTextureId: String, - tipScale: Float, - @AngleDegreesFloat tipRotationDegrees: Float, - tipParticleGapDistanceScale: Float, - positionOffsetRangeStart: Float, - positionOffsetRangeEnd: Float, - distanceTraveledRangeStart: Float, - distanceTraveledRangeEnd: Float, - luminosityRangeStart: Float, - luminosityRangeEnd: Float, - ): BrushCoat = - BrushCoat( - tip = - BrushTip( - scaleX = tipScale, - scaleY = tipScale, - cornerRounding = 0f, - rotationDegrees = tipRotationDegrees, - particleGapDistanceScale = tipParticleGapDistanceScale, - behaviors = - listOf( - BrushBehavior( - terminalNodes = - listOf( - BrushBehavior.TargetNode( - target = Target.SIZE_MULTIPLIER, - targetModifierRangeStart = 1.0f, - targetModifierRangeEnd = 0.0f, - input = - BrushBehavior.SourceNode( - source = Source.TIME_SINCE_INPUT_IN_SECONDS, - sourceValueRangeStart = 0.0f, - sourceValueRangeEnd = 0.7f, - sourceOutOfRangeBehavior = OutOfRange.CLAMP, - ), - ) - ) - ), - BrushBehavior( - terminalNodes = - listOf( - BrushBehavior.TargetNode( - target = BrushBehavior.Target.HUE_OFFSET_IN_RADIANS, - Angle.degreesToRadians(59f), - Angle.degreesToRadians(60f), - input = BrushBehavior.ConstantNode(value = 0f), - ), - BrushBehavior.TargetNode( - target = BrushBehavior.Target.LUMINOSITY, - luminosityRangeStart, - luminosityRangeEnd, - input = BrushBehavior.ConstantNode(value = 0f), - ), - ) - ), - BrushBehavior( - terminalNodes = - listOf( - BrushBehavior.TargetNode( - target = - BrushBehavior.Target - .POSITION_OFFSET_Y_IN_MULTIPLES_OF_BRUSH_SIZE, - positionOffsetRangeStart, - positionOffsetRangeEnd, - input = - BrushBehavior.ResponseNode( - responseCurve = - EasingFunction.Predefined.LINEAR, - input = - BrushBehavior.SourceNode( - source = - BrushBehavior.Source - .DISTANCE_TRAVELED_IN_MULTIPLES_OF_BRUSH_SIZE, - distanceTraveledRangeStart, - distanceTraveledRangeEnd, - BrushBehavior.OutOfRange.REPEAT, - ), - ), - ) - ) - ), - ), - ), - paint = - BrushPaint( - textureLayers = - listOf( - TextureLayer( - clientTextureId = clientTextureId, - sizeX = 1f, - sizeY = 1f, - opacity = 0.4f, - mapping = TextureMapping.STAMPING, - blendMode = BlendMode.MODULATE, - ) - ) - ), - ) - - private fun emojiHighlighterV1( - clientTextureId: String, - showMiniEmojiTrail: Boolean, - selfOverlap: SelfOverlap, - ): BrushFamily { - return BrushFamily( - coats = - buildList() { - add( - // Highlighter coat. - BrushCoat( - tip = - BrushTip( - scaleX = 1f, - scaleY = 1f, - cornerRounding = 1f, - behaviors = - listOf( - BrushBehavior( - terminalNodes = - listOf( - BrushBehavior.TargetNode( - target = Target.OPACITY_MULTIPLIER, - targetModifierRangeStart = 1.0f, - targetModifierRangeEnd = 0.3f, - input = - BrushBehavior.BinaryOpNode( - operation = - BrushBehavior.BinaryOp - .PRODUCT, - firstInput = - BrushBehavior.SourceNode( - source = - Source - .PREDICTED_TIME_ELAPSED_IN_MILLIS, - sourceValueRangeStart = - 0.0f, - sourceValueRangeEnd = - 24.0f, - sourceOutOfRangeBehavior = - OutOfRange.CLAMP, - ), - secondInput = - BrushBehavior.ResponseNode( - responseCurve = - EasingFunction - .Predefined - .EASE_IN_OUT, - input = - BrushBehavior - .SourceNode( - source = - Source - .PREDICTED_DISTANCE_TRAVELED_IN_MULTIPLES_OF_BRUSH_SIZE, - sourceValueRangeStart = - 1.5f, - sourceValueRangeEnd = - 2.0f, - sourceOutOfRangeBehavior = - OutOfRange - .CLAMP, - ), - ), - ), - ) - ) - ), - BrushBehavior( - terminalNodes = - listOf( - BrushBehavior.TargetNode( - target = Target.OPACITY_MULTIPLIER, - targetModifierRangeStart = 1.2f, - targetModifierRangeEnd = 1.0f, - input = - BrushBehavior.DampingNode( - dampingSource = - BrushBehavior.DampingSource - .TIME_IN_SECONDS, - dampingGap = 0.01f, - input = - BrushBehavior.SourceNode( - source = - Source - .DISTANCE_TRAVELED_IN_MULTIPLES_OF_BRUSH_SIZE, - sourceValueRangeStart = - 0.0f, - sourceValueRangeEnd = - 2.0f, - sourceOutOfRangeBehavior = - OutOfRange.CLAMP, - ), - ), - ) - ) - ), - BrushBehavior( - terminalNodes = - listOf( - BrushBehavior.TargetNode( - target = Target.OPACITY_MULTIPLIER, - targetModifierRangeStart = 1.2f, - targetModifierRangeEnd = 1.0f, - input = - BrushBehavior.DampingNode( - dampingSource = - BrushBehavior.DampingSource - .TIME_IN_SECONDS, - dampingGap = 0.01f, - input = - BrushBehavior.SourceNode( - source = - Source - .DISTANCE_REMAINING_IN_MULTIPLES_OF_BRUSH_SIZE, - sourceValueRangeStart = - 0.4f, - sourceValueRangeEnd = - 2.4f, - sourceOutOfRangeBehavior = - OutOfRange.CLAMP, - ), - ), - ) - ) - ), - BrushBehavior( - terminalNodes = - listOf( - BrushBehavior.TargetNode( - target = Target.SIZE_MULTIPLIER, - targetModifierRangeStart = 1.0f, - targetModifierRangeEnd = 0.04f, - input = - BrushBehavior.DampingNode( - dampingSource = - BrushBehavior.DampingSource - .TIME_IN_SECONDS, - dampingGap = 0.01f, - input = - BrushBehavior.SourceNode( - source = - Source - .DISTANCE_REMAINING_IN_MULTIPLES_OF_BRUSH_SIZE, - sourceValueRangeStart = - 0.3f, - sourceValueRangeEnd = - 0.0f, - sourceOutOfRangeBehavior = - OutOfRange.CLAMP, - ), - ), - ) - ) - ), - ), - ), - paint = BrushPaint(selfOverlap = selfOverlap), - ) - ) - // Minimoji trail coats. - if (showMiniEmojiTrail) { - add( - miniEmojiCoat( - clientTextureId = clientTextureId, - tipScale = 0.4f, - tipRotationDegrees = 0f, - tipParticleGapDistanceScale = 1.0f, - luminosityRangeStart = 0.48f, - luminosityRangeEnd = 2.0f, - positionOffsetRangeStart = -0.35f, - positionOffsetRangeEnd = 0.35f, - distanceTraveledRangeStart = 0f, - distanceTraveledRangeEnd = 0.22f, - ) - ) - add( - miniEmojiCoat( - clientTextureId = clientTextureId, - tipScale = 0.3f, - tipRotationDegrees = -35f, - tipParticleGapDistanceScale = 1.3f, - luminosityRangeStart = 0.8f, - luminosityRangeEnd = 2.0f, - positionOffsetRangeStart = -0.4f, - positionOffsetRangeEnd = 0.32f, - distanceTraveledRangeStart = 0.1f, - distanceTraveledRangeEnd = 0.74f, - ) - ) - add( - miniEmojiCoat( - clientTextureId = clientTextureId, - tipScale = 0.45f, - tipRotationDegrees = 45f, - tipParticleGapDistanceScale = 1.8f, - luminosityRangeStart = 0.8f, - luminosityRangeEnd = 2.0f, - positionOffsetRangeStart = -0.25f, - positionOffsetRangeEnd = 0.25f, - distanceTraveledRangeStart = 0.01f, - distanceTraveledRangeEnd = 0.74f, - ) - ) - } - - // Emoji stamp coat. - add( - BrushCoat( - tip = - BrushTip( - scaleX = EMOJI_STAMP_SCALE, - scaleY = EMOJI_STAMP_SCALE, - cornerRounding = 0f, - behaviors = - listOf( - BrushBehavior( - source = - Source - .DISTANCE_REMAINING_IN_MULTIPLES_OF_BRUSH_SIZE, - target = Target.SIZE_MULTIPLIER, - sourceValueRangeStart = 0.01f, - sourceValueRangeEnd = 0f, - targetModifierRangeStart = 0f, - targetModifierRangeEnd = 1f, - sourceOutOfRangeBehavior = OutOfRange.CLAMP, - ) - ), - ), - paint = - BrushPaint( - listOf( - TextureLayer( - clientTextureId = clientTextureId, - sizeX = EMOJI_STAMP_SCALE, - sizeY = EMOJI_STAMP_SCALE, - offsetX = -0.5f, - offsetY = -0.5f, - sizeUnit = TextureSizeUnit.BRUSH_SIZE, - origin = TextureOrigin.LAST_STROKE_INPUT, - wrapX = TextureWrap.CLAMP, - wrapY = TextureWrap.CLAMP, - blendMode = BlendMode.SRC, - ) - ) - ), - ) - ) - }, - inputModel = STOCK_INPUT_MODEL, - ) - } - /** * Factory function for constructing an emoji highlighter brush. * @@ -862,15 +267,43 @@ public object StockBrushes { showMiniEmojiTrail: Boolean = false, selfOverlap: SelfOverlap = SelfOverlap.ANY, version: EmojiHighlighterVersion = EmojiHighlighterVersion.LATEST, - ): BrushFamily = - when (version) { - EmojiHighlighterVersion.V1 -> - emojiHighlighterV1( - clientTextureId = clientTextureId, - showMiniEmojiTrail = showMiniEmojiTrail, - selfOverlap = selfOverlap, - ) - else -> - throw IllegalArgumentException("Unsupported emoji highlighter version: $version") + ): BrushFamily { + if (!(version in listOf(EmojiHighlighterVersion.V1))) { + throw IllegalArgumentException("Unsupported emoji highlighter version: ${version}") } + return BrushFamily.wrapNative( + StockBrushesNative.emojiHighlighter( + clientTextureId, + showMiniEmojiTrail, + selfOverlap.value, + version.value, + ) + ) + } +} + +/** Singleton wrapper around native JNI calls. */ +@UsedByNative +private object StockBrushesNative { + init { + NativeLoader.load() + } + + @UsedByNative external fun marker(version: Int): Long + + @UsedByNative external fun pressurePen(version: Int): Long + + @UsedByNative external fun highlighter(selfOverlap: Int, version: Int): Long + + @UsedByNative external fun dashedLine(version: Int): Long + + @UsedByNative + external fun emojiHighlighter( + clientTextureId: String, + showMiniEmojiTrail: Boolean, + selfOverlap: Int, + version: Int, + ): Long + + @UsedByNative external fun predictionFadeOutBehavior(): Long } diff --git a/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/TextureAnimationProgressHelper.kt b/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/TextureAnimationProgressHelper.kt index cc33ff901a881..1b551703da477 100644 --- a/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/TextureAnimationProgressHelper.kt +++ b/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/TextureAnimationProgressHelper.kt @@ -32,7 +32,7 @@ import androidx.annotation.RestrictTo * TODO: b/267164444 - Support texture layers within the same coat having different animation specs. */ @ExperimentalInkCustomBrushApi -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi public object TextureAnimationProgressHelper { /** @@ -74,16 +74,20 @@ public object TextureAnimationProgressHelper { } /** - * Extract the animation duration from a [BrushFamily]. If it does not support animation, then a - * duration of 0 will be returned. + * Extract the first non-zero animation duration from a [BrushFamily]. If it does not support + * animation, then a duration of 0 will be returned. */ @IntRange(from = 0) public fun getAnimationDurationMillis(brushFamily: BrushFamily): Long { - val firstTextureLayer = brushFamily.coats[0].paintPreferences[0].textureLayers.getOrNull(0) - return if (firstTextureLayer != null && firstTextureLayer.animationFrames > 1) { - firstTextureLayer.animationDurationMillis - } else { - 0 + for (coat in brushFamily.coats) { + for (paintPreference in coat.paintPreferences) { + for (textureLayer in paintPreference.textureLayers) { + if (textureLayer.animationFrames > 1) { + return textureLayer.animationDurationMillis + } + } + } } + return 0 } } diff --git a/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/color/colorspace/ColorSpace.kt b/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/color/colorspace/ColorSpace.kt index 7ecd5d785558b..1a4ca84380524 100644 --- a/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/color/colorspace/ColorSpace.kt +++ b/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/color/colorspace/ColorSpace.kt @@ -352,14 +352,14 @@ public abstract class ColorSpace( * * @see id */ - internal const val MIN_ID = -1 // Do not change + internal const val MIN_ID: Int = -1 // Do not change /** * The maximum ID value a color space can have. * * @see id */ - internal const val MAX_ID = 63 // Do not change, used to encode in longs + internal const val MAX_ID: Int = 63 // Do not change, used to encode in longs } } diff --git a/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/color/colorspace/ColorSpaces.kt b/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/color/colorspace/ColorSpaces.kt index 33ef94a28652b..5d2e4e224dd53 100644 --- a/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/color/colorspace/ColorSpaces.kt +++ b/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/color/colorspace/ColorSpaces.kt @@ -30,6 +30,7 @@ public object ColorSpaces { internal val SrgbPrimaries = floatArrayOf(0.640f, 0.330f, 0.300f, 0.600f, 0.150f, 0.060f) internal val Ntsc1953Primaries = floatArrayOf(0.67f, 0.33f, 0.21f, 0.71f, 0.14f, 0.08f) internal val Bt2020Primaries = floatArrayOf(0.708f, 0.292f, 0.170f, 0.797f, 0.131f, 0.046f) + internal val SrgbTransferParameters = TransferParameters(2.4, 1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045) private val NoneTransferParameters = @@ -362,7 +363,7 @@ public object ColorSpaces { internal inline fun getColorSpace(id: Int): ColorSpace = ColorSpacesArray[id] /** These MUST be in the order of their IDs */ - internal val ColorSpacesArray = + internal val ColorSpacesArray: Array = arrayOf( Srgb, LinearSrgb, diff --git a/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/color/colorspace/Rgb.kt b/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/color/colorspace/Rgb.kt index 28aa204e79917..4b228c577f381 100644 --- a/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/color/colorspace/Rgb.kt +++ b/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/color/colorspace/Rgb.kt @@ -217,7 +217,7 @@ public constructor( oetfOrig(x).coerceIn(min.toDouble(), max.toDouble()) } - internal val oetfFunc: DoubleFunction = DoubleFunction { x -> + internal val oetfFunc = DoubleFunction { x -> oetfOrig(x).coerceIn(min.toDouble(), max.toDouble()) } diff --git a/ink/ink-brush/src/jvmMain/kotlin/androidx/ink/brush/TextureBufferedImageStore.jvm.kt b/ink/ink-brush/src/jvmMain/kotlin/androidx/ink/brush/TextureBufferedImageStore.jvm.kt index 116cb7c0427ac..1a3942fc13b96 100644 --- a/ink/ink-brush/src/jvmMain/kotlin/androidx/ink/brush/TextureBufferedImageStore.jvm.kt +++ b/ink/ink-brush/src/jvmMain/kotlin/androidx/ink/brush/TextureBufferedImageStore.jvm.kt @@ -24,7 +24,7 @@ import java.awt.image.BufferedImage * Interface for a callback to allow the client to provide a particular [Bitmap] corresponding to a * client-provided texture ID. */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi @UsedByNative public fun interface TextureBufferedImageStore { /** diff --git a/ink/ink-brush/src/jvmTest/kotlin/androidx/ink/brush/BrushBehaviorTest.kt b/ink/ink-brush/src/jvmTest/kotlin/androidx/ink/brush/BrushBehaviorTest.kt index 6ca641932184f..89d2db677cf1d 100644 --- a/ink/ink-brush/src/jvmTest/kotlin/androidx/ink/brush/BrushBehaviorTest.kt +++ b/ink/ink-brush/src/jvmTest/kotlin/androidx/ink/brush/BrushBehaviorTest.kt @@ -252,13 +252,13 @@ class BrushBehaviorTest { } @Test - fun dampingSourceToString_returnsCorrectString() { - assertThat(BrushBehavior.DampingSource.DISTANCE_IN_CENTIMETERS.toString()) - .isEqualTo("BrushBehavior.DampingSource.DISTANCE_IN_CENTIMETERS") - assertThat(BrushBehavior.DampingSource.DISTANCE_IN_MULTIPLES_OF_BRUSH_SIZE.toString()) - .isEqualTo("BrushBehavior.DampingSource.DISTANCE_IN_MULTIPLES_OF_BRUSH_SIZE") - assertThat(BrushBehavior.DampingSource.TIME_IN_SECONDS.toString()) - .isEqualTo("BrushBehavior.DampingSource.TIME_IN_SECONDS") + fun progressDomainToString_returnsCorrectString() { + assertThat(BrushBehavior.ProgressDomain.DISTANCE_IN_CENTIMETERS.toString()) + .isEqualTo("BrushBehavior.ProgressDomain.DISTANCE_IN_CENTIMETERS") + assertThat(BrushBehavior.ProgressDomain.DISTANCE_IN_MULTIPLES_OF_BRUSH_SIZE.toString()) + .isEqualTo("BrushBehavior.ProgressDomain.DISTANCE_IN_MULTIPLES_OF_BRUSH_SIZE") + assertThat(BrushBehavior.ProgressDomain.TIME_IN_SECONDS.toString()) + .isEqualTo("BrushBehavior.ProgressDomain.TIME_IN_SECONDS") } @Test @@ -361,57 +361,57 @@ class BrushBehaviorTest { assertFailsWith { BrushBehavior.NoiseNode( 12345, - BrushBehavior.DampingSource.TIME_IN_SECONDS, + BrushBehavior.ProgressDomain.TIME_IN_SECONDS, Float.POSITIVE_INFINITY, ) } assertFailsWith { - BrushBehavior.NoiseNode(12345, BrushBehavior.DampingSource.TIME_IN_SECONDS, Float.NaN) + BrushBehavior.NoiseNode(12345, BrushBehavior.ProgressDomain.TIME_IN_SECONDS, Float.NaN) } } @Test fun noiseNodeConstructor_throwsForNegativeBasePeriod() { assertFailsWith { - BrushBehavior.NoiseNode(12345, BrushBehavior.DampingSource.TIME_IN_SECONDS, -1f) + BrushBehavior.NoiseNode(12345, BrushBehavior.ProgressDomain.TIME_IN_SECONDS, -1f) } } @Test fun noiseNodeToString() { - val node = BrushBehavior.NoiseNode(12345, BrushBehavior.DampingSource.TIME_IN_SECONDS, 1f) + val node = BrushBehavior.NoiseNode(12345, BrushBehavior.ProgressDomain.TIME_IN_SECONDS, 1f) assertThat(node.toString()).isEqualTo("NoiseNode(12345, TIME_IN_SECONDS, 1.0)") } @Test fun noiseNodeEquals_checksEqualityOfValues() { - val node = BrushBehavior.NoiseNode(12345, BrushBehavior.DampingSource.TIME_IN_SECONDS, 1f) + val node = BrushBehavior.NoiseNode(12345, BrushBehavior.ProgressDomain.TIME_IN_SECONDS, 1f) assertThat(node) .isEqualTo( - BrushBehavior.NoiseNode(12345, BrushBehavior.DampingSource.TIME_IN_SECONDS, 1f) + BrushBehavior.NoiseNode(12345, BrushBehavior.ProgressDomain.TIME_IN_SECONDS, 1f) ) assertThat(node) .isNotEqualTo( - BrushBehavior.NoiseNode(12346, BrushBehavior.DampingSource.TIME_IN_SECONDS, 1f) + BrushBehavior.NoiseNode(12346, BrushBehavior.ProgressDomain.TIME_IN_SECONDS, 1f) ) assertThat(node) .isNotEqualTo( BrushBehavior.NoiseNode( 12345, - BrushBehavior.DampingSource.DISTANCE_IN_CENTIMETERS, + BrushBehavior.ProgressDomain.DISTANCE_IN_CENTIMETERS, 1f, ) ) assertThat(node) .isNotEqualTo( - BrushBehavior.NoiseNode(12345, BrushBehavior.DampingSource.TIME_IN_SECONDS, 2f) + BrushBehavior.NoiseNode(12345, BrushBehavior.ProgressDomain.TIME_IN_SECONDS, 2f) ) } @Test fun noiseNodeHashCode_withIdenticalValues_match() { - val node1 = BrushBehavior.NoiseNode(12345, BrushBehavior.DampingSource.TIME_IN_SECONDS, 1f) - val node2 = BrushBehavior.NoiseNode(12345, BrushBehavior.DampingSource.TIME_IN_SECONDS, 1f) + val node1 = BrushBehavior.NoiseNode(12345, BrushBehavior.ProgressDomain.TIME_IN_SECONDS, 1f) + val node2 = BrushBehavior.NoiseNode(12345, BrushBehavior.ProgressDomain.TIME_IN_SECONDS, 1f) assertThat(node1.hashCode()).isEqualTo(node2.hashCode()) } @@ -543,13 +543,17 @@ class BrushBehaviorTest { val input = BrushBehavior.ConstantNode(0f) assertFailsWith { BrushBehavior.DampingNode( - BrushBehavior.DampingSource.TIME_IN_SECONDS, + BrushBehavior.ProgressDomain.TIME_IN_SECONDS, Float.POSITIVE_INFINITY, input, ) } assertFailsWith { - BrushBehavior.DampingNode(BrushBehavior.DampingSource.TIME_IN_SECONDS, Float.NaN, input) + BrushBehavior.DampingNode( + BrushBehavior.ProgressDomain.TIME_IN_SECONDS, + Float.NaN, + input, + ) } } @@ -557,21 +561,23 @@ class BrushBehaviorTest { fun dampingNodeConstructor_throwsForNegativeDampingGap() { val input = BrushBehavior.ConstantNode(0f) assertFailsWith { - BrushBehavior.DampingNode(BrushBehavior.DampingSource.TIME_IN_SECONDS, -1f, input) + BrushBehavior.DampingNode(BrushBehavior.ProgressDomain.TIME_IN_SECONDS, -1f, input) } } @Test fun dampingNodeInputs_containsInput() { val input = BrushBehavior.ConstantNode(0f) - val node = BrushBehavior.DampingNode(BrushBehavior.DampingSource.TIME_IN_SECONDS, 1f, input) + val node = + BrushBehavior.DampingNode(BrushBehavior.ProgressDomain.TIME_IN_SECONDS, 1f, input) assertThat(node.inputs).containsExactly(input) } @Test fun dampingNodeToString() { val input = BrushBehavior.ConstantNode(0f) - val node = BrushBehavior.DampingNode(BrushBehavior.DampingSource.TIME_IN_SECONDS, 1f, input) + val node = + BrushBehavior.DampingNode(BrushBehavior.ProgressDomain.TIME_IN_SECONDS, 1f, input) assertThat(node.toString()) .isEqualTo("DampingNode(TIME_IN_SECONDS, 1.0, ConstantNode(0.0))") } @@ -580,19 +586,19 @@ class BrushBehaviorTest { fun dampingNodeEquals_checksEqualityOfValues() { val node1 = BrushBehavior.DampingNode( - BrushBehavior.DampingSource.TIME_IN_SECONDS, + BrushBehavior.ProgressDomain.TIME_IN_SECONDS, 1f, BrushBehavior.ConstantNode(1f), ) val node2 = BrushBehavior.DampingNode( - BrushBehavior.DampingSource.TIME_IN_SECONDS, + BrushBehavior.ProgressDomain.TIME_IN_SECONDS, 1f, BrushBehavior.ConstantNode(1f), ) val node3 = BrushBehavior.DampingNode( - BrushBehavior.DampingSource.TIME_IN_SECONDS, + BrushBehavior.ProgressDomain.TIME_IN_SECONDS, 1f, BrushBehavior.ConstantNode(2f), ) @@ -604,19 +610,19 @@ class BrushBehaviorTest { fun dampingNodeHashCode_withIdenticalValues_match() { val node1 = BrushBehavior.DampingNode( - BrushBehavior.DampingSource.TIME_IN_SECONDS, + BrushBehavior.ProgressDomain.TIME_IN_SECONDS, 1f, BrushBehavior.ConstantNode(1f), ) val node2 = BrushBehavior.DampingNode( - BrushBehavior.DampingSource.TIME_IN_SECONDS, + BrushBehavior.ProgressDomain.TIME_IN_SECONDS, 1f, BrushBehavior.ConstantNode(1f), ) val node3 = BrushBehavior.DampingNode( - BrushBehavior.DampingSource.TIME_IN_SECONDS, + BrushBehavior.ProgressDomain.TIME_IN_SECONDS, 1f, BrushBehavior.ConstantNode(2f), ) @@ -681,6 +687,132 @@ class BrushBehaviorTest { assertThat(node1.hashCode()).isNotEqualTo(node3.hashCode()) } + @Test + fun integralNodeConstructor_throwsForNonFiniteValueRange() { + val input = BrushBehavior.ConstantNode(0f) + assertFailsWith { + BrushBehavior.IntegralNode( + BrushBehavior.ProgressDomain.TIME_IN_SECONDS, + integralValueRangeStart = Float.NaN, + integralValueRangeEnd = 5f, + BrushBehavior.OutOfRange.REPEAT, + input, + ) + } + assertFailsWith { + BrushBehavior.IntegralNode( + BrushBehavior.ProgressDomain.TIME_IN_SECONDS, + integralValueRangeStart = 0f, + integralValueRangeEnd = Float.POSITIVE_INFINITY, + BrushBehavior.OutOfRange.REPEAT, + input, + ) + } + } + + @Test + fun integralNodeConstructor_throwsForEmptyValueRange() { + val input = BrushBehavior.ConstantNode(0f) + assertFailsWith { + BrushBehavior.IntegralNode( + BrushBehavior.ProgressDomain.TIME_IN_SECONDS, + integralValueRangeStart = 5f, + integralValueRangeEnd = 5f, + BrushBehavior.OutOfRange.REPEAT, + input, + ) + } + } + + @Test + fun integralNodeInputs_containsInput() { + val input = BrushBehavior.ConstantNode(0f) + val node = + BrushBehavior.IntegralNode( + BrushBehavior.ProgressDomain.TIME_IN_SECONDS, + integralValueRangeStart = 0f, + integralValueRangeEnd = 5f, + BrushBehavior.OutOfRange.REPEAT, + input, + ) + assertThat(node.inputs).containsExactly(input) + } + + @Test + fun integralNodeToString() { + val input = BrushBehavior.ConstantNode(0f) + val node = + BrushBehavior.IntegralNode( + BrushBehavior.ProgressDomain.TIME_IN_SECONDS, + integralValueRangeStart = 0f, + integralValueRangeEnd = 5f, + BrushBehavior.OutOfRange.REPEAT, + input, + ) + assertThat(node.toString()) + .isEqualTo("IntegralNode(TIME_IN_SECONDS, 0.0, 5.0, REPEAT, ConstantNode(0.0))") + } + + @Test + fun integralNodeEquals_checksEqualityOfValues() { + val node1 = + BrushBehavior.IntegralNode( + BrushBehavior.ProgressDomain.TIME_IN_SECONDS, + integralValueRangeStart = 0f, + integralValueRangeEnd = 5f, + BrushBehavior.OutOfRange.REPEAT, + BrushBehavior.ConstantNode(1f), + ) + val node2 = + BrushBehavior.IntegralNode( + BrushBehavior.ProgressDomain.TIME_IN_SECONDS, + integralValueRangeStart = 0f, + integralValueRangeEnd = 5f, + BrushBehavior.OutOfRange.REPEAT, + BrushBehavior.ConstantNode(1f), + ) + val node3 = + BrushBehavior.IntegralNode( + BrushBehavior.ProgressDomain.TIME_IN_SECONDS, + integralValueRangeStart = 0f, + integralValueRangeEnd = 5f, + BrushBehavior.OutOfRange.REPEAT, + BrushBehavior.ConstantNode(2f), + ) + assertThat(node1).isEqualTo(node2) + assertThat(node1).isNotEqualTo(node3) + } + + @Test + fun integralNodeHashCode_withIdenticalValues_match() { + val node1 = + BrushBehavior.IntegralNode( + BrushBehavior.ProgressDomain.TIME_IN_SECONDS, + integralValueRangeStart = 0f, + integralValueRangeEnd = 5f, + BrushBehavior.OutOfRange.REPEAT, + BrushBehavior.ConstantNode(1f), + ) + val node2 = + BrushBehavior.IntegralNode( + BrushBehavior.ProgressDomain.TIME_IN_SECONDS, + integralValueRangeStart = 0f, + integralValueRangeEnd = 5f, + BrushBehavior.OutOfRange.REPEAT, + BrushBehavior.ConstantNode(1f), + ) + val node3 = + BrushBehavior.IntegralNode( + BrushBehavior.ProgressDomain.TIME_IN_SECONDS, + integralValueRangeStart = 0f, + integralValueRangeEnd = 5f, + BrushBehavior.OutOfRange.REPEAT, + BrushBehavior.ConstantNode(2f), + ) + assertThat(node1.hashCode()).isEqualTo(node2.hashCode()) + assertThat(node1.hashCode()).isNotEqualTo(node3.hashCode()) + } + @Test fun binaryOpNodeInputs_containsInputsInOrder() { val firstInput = BrushBehavior.ConstantNode(0f) @@ -1291,13 +1423,14 @@ class BrushBehaviorTest { sourceValueRangeEnd = 1.0f, ), ) - ) + ), + developerComment = "foobar", ) .toString() ) .isEqualTo( "BrushBehavior([TargetNode(WIDTH_MULTIPLIER, 1.0, 1.75, " + - "SourceNode(NORMALIZED_PRESSURE, 0.0, 1.0, CLAMP))])" + "SourceNode(NORMALIZED_PRESSURE, 0.0, 1.0, CLAMP))], developerComment=foobar)" ) } diff --git a/ink/ink-brush/src/jvmTest/kotlin/androidx/ink/brush/BrushFamilyTest.kt b/ink/ink-brush/src/jvmTest/kotlin/androidx/ink/brush/BrushFamilyTest.kt index 04ca3009c5d26..ad32b1279e1c8 100644 --- a/ink/ink-brush/src/jvmTest/kotlin/androidx/ink/brush/BrushFamilyTest.kt +++ b/ink/ink-brush/src/jvmTest/kotlin/androidx/ink/brush/BrushFamilyTest.kt @@ -27,7 +27,8 @@ import org.junit.runners.JUnit4 class BrushFamilyTest { @Test fun constructor_withValidArguments_returnsABrushFamily() { - assertThat(BrushFamily(customTip, customPaint, customBrushFamilyId)).isNotNull() + assertThat(BrushFamily(customTip, customPaint, clientBrushFamilyId = customBrushFamilyId)) + .isNotNull() } @Test @@ -49,14 +50,24 @@ class BrushFamilyTest { @Test fun equals_comparesValues() { val brushFamily = - BrushFamily(customTip, customPaint, customBrushFamilyId, BrushFamily.SPRING_MODEL) + BrushFamily( + customTip, + customPaint, + inputModel = BrushFamily.SPRING_MODEL, + clientBrushFamilyId = customBrushFamilyId, + ) val differentCoat = BrushCoat(BrushTip(), BrushPaint()) val differentId = "different" // same values are equal. assertThat(brushFamily) .isEqualTo( - BrushFamily(customTip, customPaint, customBrushFamilyId, BrushFamily.SPRING_MODEL) + BrushFamily( + tip = customTip, + paint = customPaint, + inputModel = BrushFamily.SPRING_MODEL, + clientBrushFamilyId = customBrushFamilyId, + ) ) // different values are not equal. @@ -75,11 +86,11 @@ class BrushFamilyTest { fun toString_returnsExpectedValues() { assertThat(BrushFamily(inputModel = BrushFamily.SPRING_MODEL).toString()) .isEqualTo( - "BrushFamily(coats=[BrushCoat(tip=BrushTip(scale=(1.0, 1.0), " + + "BrushFamily(developerComment=, coats=[BrushCoat(tip=BrushTip(scale=(1.0, 1.0), " + "cornerRounding=1.0, slantDegrees=0.0, pinch=0.0, rotationDegrees=0.0, " + "particleGapDistanceScale=0.0, particleGapDurationMillis=0, behaviors=[]), " + "paintPreferences=[BrushPaint(textureLayers=[], colorFunctions=[], " + - "selfOverlap=SelfOverlap.ANY)])], clientBrushFamilyId=, inputModel=SpringModel)" + "selfOverlap=SelfOverlap.ANY)])], inputModel=SpringModel, clientBrushFamilyId=)" ) } @@ -156,7 +167,8 @@ class BrushFamilyTest { @Test fun copy_whenSameContents_returnsSameInstance() { - val customFamily = BrushFamily(customTip, customPaint, customBrushFamilyId) + val customFamily = + BrushFamily(customTip, customPaint, clientBrushFamilyId = customBrushFamilyId) // A pure copy returns `this`. val copy = customFamily.copy() @@ -165,24 +177,28 @@ class BrushFamilyTest { @Test fun copy_withArguments_createsCopyWithChanges() { - val brushFamily = BrushFamily(customTip, customPaint, customBrushFamilyId) + val brushFamily = + BrushFamily(customTip, customPaint, clientBrushFamilyId = customBrushFamilyId) val differentCoats = listOf(BrushCoat(BrushTip(), BrushPaint())) val differentId = "different" assertThat(brushFamily.copy(coats = differentCoats)) - .isEqualTo(BrushFamily(differentCoats, customBrushFamilyId)) + .isEqualTo(BrushFamily(differentCoats, clientBrushFamilyId = customBrushFamilyId)) assertThat(brushFamily.copy(clientBrushFamilyId = differentId)) - .isEqualTo(BrushFamily(customTip, customPaint, differentId)) + .isEqualTo(BrushFamily(customTip, customPaint, clientBrushFamilyId = differentId)) } @Test fun builder_createsExpectedBrushFamily() { val family = BrushFamily.Builder() - .setCoat(customTip, customPaint) + .setCoats(listOf(BrushCoat(customTip, customPaint))) .setClientBrushFamilyId(customBrushFamilyId) .build() - assertThat(family).isEqualTo(BrushFamily(customTip, customPaint, customBrushFamilyId)) + assertThat(family) + .isEqualTo( + BrushFamily(customTip, customPaint, clientBrushFamilyId = customBrushFamilyId) + ) } /** @@ -276,5 +292,5 @@ class BrushFamilyTest { /** Brush Family with every field different from default values. */ private fun newCustomBrushFamily(): BrushFamily = - BrushFamily(customTip, customPaint, customBrushFamilyId) + BrushFamily(customTip, customPaint, clientBrushFamilyId = customBrushFamilyId) } diff --git a/ink/ink-brush/src/jvmTest/kotlin/androidx/ink/brush/BrushTest.kt b/ink/ink-brush/src/jvmTest/kotlin/androidx/ink/brush/BrushTest.kt index 9f69893e02f3b..66a2f449e8011 100644 --- a/ink/ink-brush/src/jvmTest/kotlin/androidx/ink/brush/BrushTest.kt +++ b/ink/ink-brush/src/jvmTest/kotlin/androidx/ink/brush/BrushTest.kt @@ -50,15 +50,18 @@ class BrushTest { @Suppress("Range") // Testing error cases. fun constructor_withBadSize_willThrow() { assertFailsWith { - Brush(family, color, -2F, epsilon) // non-positive size. + // non-positive size + Brush.createWithColorIntArgb(family, color.toArgb(), -2F, epsilon) } assertFailsWith { - Brush(family, color, Float.POSITIVE_INFINITY, epsilon) // non-finite size. + // non-finite size + Brush.createWithColorIntArgb(family, color.toArgb(), Float.POSITIVE_INFINITY, epsilon) } assertFailsWith { - Brush(family, color, Float.NaN, epsilon) // non-finite size. + // non-finite size + Brush.createWithColorIntArgb(family, color.toArgb(), Float.NaN, epsilon) } } @@ -66,15 +69,18 @@ class BrushTest { @Suppress("Range") // Testing error cases. fun constructor_withBadEpsilon_willThrow() { assertFailsWith { - Brush(family, color, size, -2F) // non-positive epsilon. + // non-positive epsilon + Brush.createWithColorIntArgb(family, color.toArgb(), size, -2F) } assertFailsWith { - Brush(family, color, size, Float.POSITIVE_INFINITY) // non-finite epsilon. + // non-finite epsilon + Brush.createWithColorIntArgb(family, color.toArgb(), size, Float.POSITIVE_INFINITY) } assertFailsWith { - Brush(family, color, size, Float.NaN) // non-finite epsilon. + // non-finite epsilon + Brush.createWithColorIntArgb(family, color.toArgb(), size, Float.NaN) } } @@ -118,8 +124,8 @@ class BrushTest { @Test fun equals_returnsTrueForIdenticalBrushes() { - val brush = Brush(family, color, size, epsilon) - val otherBrush = Brush(family, color, size, epsilon) + val brush = Brush.createWithColorIntArgb(family, color.toArgb(), size, epsilon) + val otherBrush = Brush.createWithColorIntArgb(family, color.toArgb(), size, epsilon) assertThat(brush == brush).isTrue() assertThat(brush == otherBrush).isTrue() assertThat(otherBrush == brush).isTrue() @@ -127,8 +133,8 @@ class BrushTest { @Test fun hashCode_isEqualForIdenticalBrushes() { - val brush = Brush(family, color, size, epsilon) - val otherBrush = Brush(family, color, size, epsilon) + val brush = Brush.createWithColorIntArgb(family, color.toArgb(), size, epsilon) + val otherBrush = Brush.createWithColorIntArgb(family, color.toArgb(), size, epsilon) assertThat(brush == brush).isTrue() assertThat(brush == otherBrush).isTrue() assertThat(otherBrush == brush).isTrue() @@ -136,10 +142,15 @@ class BrushTest { @Test fun equals_returnsFalseIfAnyFieldsDiffer() { - val brush = Brush(family, color, size, epsilon) + val brush = Brush.createWithColorIntArgb(family, color.toArgb(), size, epsilon) val differentFamilyBrush = - Brush(BrushFamily(clientBrushFamilyId = "/brush-family:pencil:1"), color, size, epsilon) + Brush.createWithColorIntArgb( + BrushFamily(clientBrushFamilyId = "/brush-family:pencil:1"), + color.toArgb(), + size, + epsilon, + ) assertThat(brush == differentFamilyBrush).isFalse() assertThat(differentFamilyBrush == brush).isFalse() assertThat(brush != differentFamilyBrush).isTrue() @@ -155,13 +166,13 @@ class BrushTest { assertThat(brush != differentcolorBrush).isTrue() assertThat(differentcolorBrush != brush).isTrue() - val differentSizeBrush = Brush(family, color, 9.0f, epsilon) + val differentSizeBrush = Brush.createWithColorIntArgb(family, color.toArgb(), 9.0f, epsilon) assertThat(brush == differentSizeBrush).isFalse() assertThat(differentSizeBrush == brush).isFalse() assertThat(brush != differentSizeBrush).isTrue() assertThat(differentSizeBrush != brush).isTrue() - val differentEpsilonBrush = Brush(family, color, size, 1.1f) + val differentEpsilonBrush = Brush.createWithColorIntArgb(family, color.toArgb(), size, 1.1f) assertThat(brush == differentEpsilonBrush).isFalse() assertThat(differentEpsilonBrush == brush).isFalse() assertThat(brush != differentEpsilonBrush).isTrue() @@ -170,10 +181,15 @@ class BrushTest { @Test fun hashCode_differsIfAnyFieldsDiffer() { - val brush = Brush(family, color, size, epsilon) + val brush = Brush.createWithColorIntArgb(family, color.toArgb(), size, epsilon) val differentFamilyBrush = - Brush(BrushFamily(clientBrushFamilyId = "/brush-family:pencil:1"), color, size, epsilon) + Brush.createWithColorIntArgb( + BrushFamily(clientBrushFamilyId = "/brush-family:pencil:1"), + color.toArgb(), + size, + epsilon, + ) assertThat(differentFamilyBrush.hashCode()).isNotEqualTo(brush.hashCode()) val otherColor = @@ -183,10 +199,10 @@ class BrushTest { val differentcolorBrush = Brush.createWithColorLong(family, otherColor, size, epsilon) assertThat(differentcolorBrush.hashCode()).isNotEqualTo(brush.hashCode()) - val differentSizeBrush = Brush(family, color, 9.0f, epsilon) + val differentSizeBrush = Brush.createWithColorIntArgb(family, color.toArgb(), 9.0f, epsilon) assertThat(differentSizeBrush.hashCode()).isNotEqualTo(brush.hashCode()) - val differentEpsilonBrush = Brush(family, color, size, 1.1f) + val differentEpsilonBrush = Brush.createWithColorIntArgb(family, color.toArgb(), size, 1.1f) assertThat(differentEpsilonBrush.hashCode()).isNotEqualTo(brush.hashCode()) } @@ -366,7 +382,7 @@ class BrushTest { /** Brush with every field different from default values. */ private fun buildTestBrush(): Brush = - Brush( + Brush.createWithColorIntArgb( BrushFamily( tip = BrushTip( @@ -398,7 +414,7 @@ class BrushTest { paint = BrushPaint(), clientBrushFamilyId = "/brush-family:marker:1", ), - color, + color.toArgb(), 13F, 0.1234F, ) diff --git a/ink/ink-geometry-compose/README.md b/ink/ink-geometry-compose/README.md new file mode 100644 index 0000000000000..e7c3e22f01456 --- /dev/null +++ b/ink/ink-geometry-compose/README.md @@ -0,0 +1,5 @@ +# Ink Geometry Compose Module + +Ink's `geometry` module with extensions specific to Jetpack Compose. Separate +from the main `geometry` module so that clients not using Compose can avoid the +Compose dependency. diff --git a/ink/ink-geometry/README.md b/ink/ink-geometry/README.md new file mode 100644 index 0000000000000..3b413b659a795 --- /dev/null +++ b/ink/ink-geometry/README.md @@ -0,0 +1,5 @@ +# Ink Geometry Module + +This module contains logic for basic geometric shapes and operations, useful for +implementing UI related to Ink strokes. General-purpose stroke geometry and +rendering-related metadata are handled by `PartitionedMesh`. diff --git a/ink/ink-geometry/src/jvmAndAndroidMain/kotlin/androidx/ink/geometry/BoxAccumulator.kt b/ink/ink-geometry/src/jvmAndAndroidMain/kotlin/androidx/ink/geometry/BoxAccumulator.kt index 433162534cd8c..5b7ef1d2238c1 100644 --- a/ink/ink-geometry/src/jvmAndAndroidMain/kotlin/androidx/ink/geometry/BoxAccumulator.kt +++ b/ink/ink-geometry/src/jvmAndAndroidMain/kotlin/androidx/ink/geometry/BoxAccumulator.kt @@ -282,7 +282,7 @@ public class BoxAccumulator { * @return `this` */ @UsedByNative - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi public fun populateFrom(x1: Float, y1: Float, x2: Float, y2: Float): BoxAccumulator { hasBounds = true _bounds.setXBounds(x1, x2).setYBounds(y1, y2) diff --git a/ink/ink-geometry/src/jvmTest/kotlin/androidx/ink/geometry/ParallelogramInterfaceTest.kt b/ink/ink-geometry/src/jvmTest/kotlin/androidx/ink/geometry/ParallelogramInterfaceTest.kt index ce35ffa335abd..5cbc00947059f 100644 --- a/ink/ink-geometry/src/jvmTest/kotlin/androidx/ink/geometry/ParallelogramInterfaceTest.kt +++ b/ink/ink-geometry/src/jvmTest/kotlin/androidx/ink/geometry/ParallelogramInterfaceTest.kt @@ -25,95 +25,59 @@ import org.junit.runners.JUnit4 class ParallelogramInterfaceTest { @Test - fun normalizeAndRun_withNegativeWidth_normalizesWidthHeightAndRotation() { - val expectedWidth = 5f - val expectedHeight = -3f - val expectedRotationDegrees = Angle.QUARTER_TURN_DEGREES + Angle.HALF_TURN_DEGREES - val assertExpectedValues: (Float, Float, Float) -> Parallelogram = - { normalizedWidth: Float, normalizedHeight: Float, normalizedRotation: Float -> - assertThat(normalizedWidth).isEqualTo(expectedWidth) - assertThat(normalizedHeight).isEqualTo(expectedHeight) - assertThat(normalizedRotation).isWithin(tolerance).of(expectedRotationDegrees) - ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( - ImmutableVec(0f, 0f), - expectedWidth, - expectedHeight, - expectedRotationDegrees, - 0f, - ) - } - Parallelogram.normalizeAndRun( - width = -5f, - height = 3f, - rotationDegrees = Angle.QUARTER_TURN_DEGREES, - runBlock = assertExpectedValues, - ) + fun fromCenterDimensionsRotationInDegreesAndSkew_withNegativeWidth_normalizes() { + val parallelogram = + ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( + center = ImmutableVec(0f, 0f), + width = -5f, + height = 3f, + rotationDegrees = Angle.QUARTER_TURN_DEGREES, + skew = 0f, + ) + assertThat(parallelogram.width).isEqualTo(5f) + assertThat(parallelogram.height).isEqualTo(-3f) + assertThat(parallelogram.rotationDegrees) + .isWithin(tolerance) + .of(Angle.QUARTER_TURN_DEGREES + Angle.HALF_TURN_DEGREES) } @Test - fun normalizeAndRun_withHighRotation_normalizesRotation() { - val expectedWidth = 5f - val expectedHeight = 3f - val expectedRotationDegrees = - Angle.QUARTER_TURN_DEGREES // 5 Pi normalized to range [0, 2*pi] - val assertExpectedValues: (Float, Float, Float) -> Parallelogram = - { normalizedWidth: Float, normalizedHeight: Float, normalizedRotation: Float -> - assertThat(normalizedWidth).isEqualTo(expectedWidth) - assertThat(normalizedHeight).isEqualTo(expectedHeight) - assertThat(normalizedRotation).isWithin(tolerance).of(expectedRotationDegrees) - ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( - ImmutableVec(0f, 0f), - expectedWidth, - expectedHeight, - expectedRotationDegrees, - 0f, - ) - } - - Parallelogram.normalizeAndRun( - width = 5f, - height = 3f, - rotationDegrees = 5 * Angle.QUARTER_TURN_DEGREES, - runBlock = assertExpectedValues, - ) + fun fromCenterDimensionsRotationInDegreesAndSkew_withHighRotation_normalizesRotation() { + val parallelogram = + ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( + center = ImmutableVec(0f, 0f), + width = 5f, + height = 3f, + rotationDegrees = 5 * Angle.QUARTER_TURN_DEGREES, + skew = 0f, + ) + assertThat(parallelogram.width).isEqualTo(5f) + assertThat(parallelogram.height).isEqualTo(3f) + assertThat(parallelogram.rotationDegrees).isWithin(tolerance).of(Angle.QUARTER_TURN_DEGREES) } @Test fun signedArea_calculatesArea() { val parallelogram = - Parallelogram.normalizeAndRun( + ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( + center = ImmutableVec(0f, 0f), width = 5f, height = 3f, rotationDegrees = Angle.QUARTER_TURN_DEGREES, - runBlock = { w: Float, h: Float, r: Float -> - ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( - ImmutableVec(0f, 0f), - w, - h, - r, - 0f, - ) - }, + skew = 0f, ) assertThat(parallelogram.computeSignedArea()).isEqualTo(15f) } @Test - fun computeBoundingBox_returnsCorrectBoundingBoxNoShear() { + fun computeBoundingBox_returnsCorrectBoundingBoxNoSkew() { val parallelogram = - Parallelogram.normalizeAndRun( + ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( + center = ImmutableVec(0f, 0f), width = 5f, height = 3f, rotationDegrees = Angle.QUARTER_TURN_DEGREES, - runBlock = { w: Float, h: Float, r: Float -> - ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( - ImmutableVec(0f, 0f), - w, - h, - r, - 0f, - ) - }, + skew = 0f, ) assertThat( parallelogram @@ -130,21 +94,14 @@ class ParallelogramInterfaceTest { } @Test - fun computeBoundingBox_populatesBoundingBoxNoShear() { + fun computeBoundingBox_populatesBoundingBoxNoSkew() { val parallelogram = - Parallelogram.normalizeAndRun( + ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( + center = ImmutableVec(0f, 0f), width = 5f, height = 3f, rotationDegrees = Angle.QUARTER_TURN_DEGREES, - runBlock = { w: Float, h: Float, r: Float -> - ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( - ImmutableVec(0f, 0f), - w, - h, - r, - 0f, - ) - }, + skew = 0f, ) val box = MutableBox() parallelogram.computeBoundingBox(box) @@ -163,19 +120,12 @@ class ParallelogramInterfaceTest { @Test fun computeBoundingBox_returnsCorrectBoundingBoxWithShear() { val parallelogram = - Parallelogram.normalizeAndRun( + ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( + center = ImmutableVec(0f, 0f), width = 5f, height = 3f, rotationDegrees = Angle.QUARTER_TURN_DEGREES, - runBlock = { w: Float, h: Float, r: Float -> - ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( - ImmutableVec(0f, 0f), - w, - h, - r, - 2f, - ) - }, + skew = 2f, ) assertThat( parallelogram @@ -194,19 +144,12 @@ class ParallelogramInterfaceTest { @Test fun computeBoundingBox_populatesBoundingBoxWithShear() { val parallelogram = - Parallelogram.normalizeAndRun( + ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( + center = ImmutableVec(0f, 0f), width = 5f, height = 3f, rotationDegrees = Angle.QUARTER_TURN_DEGREES, - runBlock = { w: Float, h: Float, r: Float -> - ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( - ImmutableVec(0f, 0f), - w, - h, - r, - 2f, - ) - }, + skew = 2f, ) val box = MutableBox() parallelogram.computeBoundingBox(box) @@ -225,20 +168,14 @@ class ParallelogramInterfaceTest { @Test fun computeSemiAxes_returnsCorrectSemiAxes() { val parallelogram = - Parallelogram.normalizeAndRun( + ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( + center = ImmutableVec(0f, 0f), width = 5f, height = 3f, rotationDegrees = Angle.HALF_TURN_DEGREES, - runBlock = { w: Float, h: Float, r: Float -> - ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( - ImmutableVec(0f, 0f), - w, - h, - r, - 2f, - ) - }, + skew = 2f, ) + val axes = parallelogram.computeSemiAxes() assertThat(axes.size).isEqualTo(2) assertThat(axes.get(0).isAlmostEqual(ImmutableVec(-2.5f, 0f), tolerance)).isTrue() @@ -248,19 +185,12 @@ class ParallelogramInterfaceTest { @Test fun computeSemiAxes_populatesSemiAxes() { val parallelogram = - Parallelogram.normalizeAndRun( + ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( + center = ImmutableVec(0f, 0f), width = 5f, height = 3f, rotationDegrees = Angle.HALF_TURN_DEGREES, - runBlock = { w: Float, h: Float, r: Float -> - ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( - ImmutableVec(0f, 0f), - w, - h, - r, - 2f, - ) - }, + skew = 2f, ) val axis1 = MutableVec() val axis2 = MutableVec() @@ -272,19 +202,12 @@ class ParallelogramInterfaceTest { @Test fun computeCorners_returnsCorrectCorners() { val parallelogram = - Parallelogram.normalizeAndRun( + ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( + center = ImmutableVec(0f, 0f), width = 5f, height = 3f, rotationDegrees = Angle.QUARTER_TURN_DEGREES, - runBlock = { w: Float, h: Float, r: Float -> - ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( - ImmutableVec(0f, 0f), - w, - h, - r, - 2f, - ) - }, + skew = 2f, ) val corners = parallelogram.computeCorners() assertThat(corners.size).isEqualTo(4) @@ -297,19 +220,12 @@ class ParallelogramInterfaceTest { @Test fun computeCorners_populatesCorrectCorners() { val parallelogram = - Parallelogram.normalizeAndRun( + ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( + center = ImmutableVec(0f, 0f), width = 5f, height = 3f, rotationDegrees = Angle.QUARTER_TURN_DEGREES, - runBlock = { w: Float, h: Float, r: Float -> - ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( - ImmutableVec(0f, 0f), - w, - h, - r, - 2f, - ) - }, + skew = 2f, ) val corner1 = MutableVec() val corner2 = MutableVec() @@ -325,19 +241,12 @@ class ParallelogramInterfaceTest { @Test fun isAlmostEqual_withToleranceGiven_returnsCorrectValue() { val parallelogram = - Parallelogram.normalizeAndRun( + ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( + center = ImmutableVec(0f, 0f), width = 5f, height = 3f, rotationDegrees = Angle.ZERO_DEGREES, - runBlock = { w: Float, h: Float, r: Float -> - ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( - ImmutableVec(0f, 0f), - w, - h, - r, - 2f, - ) - }, + skew = 2f, ) val corner1 = MutableVec() val corner2 = MutableVec() @@ -354,19 +263,12 @@ class ParallelogramInterfaceTest { @Test fun contains_returnsCorrectValue() { val parallelogram = - Parallelogram.normalizeAndRun( + ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( + center = ImmutableVec(0f, 0f), width = 5f, height = 3f, rotationDegrees = Angle.ZERO_DEGREES, - runBlock = { w: Float, h: Float, r: Float -> - ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( - ImmutableVec(0f, 0f), - w, - h, - r, - 2f, - ) - }, + skew = 2f, ) // Center of the parallelogram assertThat(parallelogram.contains(ImmutableVec(0f, 0f))).isTrue() @@ -379,34 +281,20 @@ class ParallelogramInterfaceTest { @Test fun isAlmostEqual_withinToleranceReturnsTrue() { val parallelogram = - Parallelogram.normalizeAndRun( + ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( + center = ImmutableVec(0f, 0f), width = 5f, height = 3f, rotationDegrees = Angle.ZERO_DEGREES, - runBlock = { w: Float, h: Float, r: Float -> - ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( - ImmutableVec(0f, 0f), - w, - h, - r, - 2f, - ) - }, + skew = 2f, ) val other = - Parallelogram.normalizeAndRun( + ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( + center = ImmutableVec(0f, 0f), width = 5.0000009f, height = 3f, rotationDegrees = Angle.ZERO_DEGREES, - runBlock = { w: Float, h: Float, r: Float -> - ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( - ImmutableVec(0f, 0f), - w, - h, - r, - 2f, - ) - }, + skew = 2f, ) assertThat(parallelogram.isAlmostEqual(other, tolerance)).isTrue() } @@ -414,34 +302,20 @@ class ParallelogramInterfaceTest { @Test fun isAlmostEqual_outsideToleranceReturnsFalse() { val parallelogram = - Parallelogram.normalizeAndRun( + ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( + center = ImmutableVec(0f, 0f), width = 5f, height = 3f, rotationDegrees = Angle.ZERO_DEGREES, - runBlock = { w: Float, h: Float, r: Float -> - ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( - ImmutableVec(0f, 0f), - w, - h, - r, - 2f, - ) - }, + skew = 2f, ) val other = - Parallelogram.normalizeAndRun( + ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( + center = ImmutableVec(0f, 0f), width = 5.000009f, height = 3f, rotationDegrees = Angle.ZERO_DEGREES, - runBlock = { w: Float, h: Float, r: Float -> - ImmutableParallelogram.fromCenterDimensionsRotationInDegreesAndSkew( - ImmutableVec(0f, 0f), - w, - h, - r, - 2f, - ) - }, + skew = 2f, ) assertThat(parallelogram.isAlmostEqual(other, 1e-6f)).isFalse() } diff --git a/ink/ink-nativeloader/README.md b/ink/ink-nativeloader/README.md new file mode 100644 index 0000000000000..23ba4b883a628 --- /dev/null +++ b/ink/ink-nativeloader/README.md @@ -0,0 +1,4 @@ +# Ink Native Loader Module + +Handles loading native code from Ink's core C++ implementation, published +[on GitHub](https://github.com/google/ink). diff --git a/ink/ink-nativeloader/ink_jni.pgcfg b/ink/ink-nativeloader/ink_jni.pgcfg index 30ad95217e79a..9097c271e2ad0 100644 --- a/ink/ink-nativeloader/ink_jni.pgcfg +++ b/ink/ink-nativeloader/ink_jni.pgcfg @@ -8,9 +8,9 @@ -if class androidx.ink.nativeloader.NativeLoader -keep class androidx.ink.nativeloader.UsedByNative -# Keep annotated class names. +# Keep annotated classes unconditionally. -if class androidx.ink.nativeloader.NativeLoader --keepnames @androidx.ink.nativeloader.UsedByNative class * { +-keep @androidx.ink.nativeloader.UsedByNative class * { (); } diff --git a/ink/ink-rendering/README.md b/ink/ink-rendering/README.md new file mode 100644 index 0000000000000..04bdf9f8d30dd --- /dev/null +++ b/ink/ink-rendering/README.md @@ -0,0 +1,5 @@ +# Ink Rendering Module + +This module provides logic for rendering freehand strokes constructed with the +`strokes` module. Currently, there is just an `android` submodule which uses +Android platform rendering APIs. diff --git a/ink/ink-rendering/api/current.txt b/ink/ink-rendering/api/current.txt index 8e6cb8a74a407..346acf55529e4 100644 --- a/ink/ink-rendering/api/current.txt +++ b/ink/ink-rendering/api/current.txt @@ -2,6 +2,7 @@ package androidx.ink.rendering.android.canvas { public interface CanvasStrokeRenderer { + method public static androidx.ink.rendering.android.canvas.CanvasStrokeRenderer create(); method public static androidx.ink.rendering.android.canvas.CanvasStrokeRenderer create(optional androidx.ink.brush.TextureBitmapStore textureStore); method public default void draw(android.graphics.Canvas canvas, androidx.ink.strokes.InProgressStroke inProgressStroke, android.graphics.Matrix strokeToScreenTransform); method public default void draw(android.graphics.Canvas canvas, androidx.ink.strokes.InProgressStroke inProgressStroke, androidx.ink.geometry.AffineTransform strokeToScreenTransform); @@ -11,6 +12,7 @@ package androidx.ink.rendering.android.canvas { } public static final class CanvasStrokeRenderer.Companion { + method public androidx.ink.rendering.android.canvas.CanvasStrokeRenderer create(); method public androidx.ink.rendering.android.canvas.CanvasStrokeRenderer create(optional androidx.ink.brush.TextureBitmapStore textureStore); method @BytecodeOnly public static androidx.ink.rendering.android.canvas.CanvasStrokeRenderer! create$default(androidx.ink.rendering.android.canvas.CanvasStrokeRenderer.Companion!, androidx.ink.brush.TextureBitmapStore!, int, Object!); } diff --git a/ink/ink-rendering/api/restricted_current.txt b/ink/ink-rendering/api/restricted_current.txt index 8dbf34d4a4fea..da1e13496d2ad 100644 --- a/ink/ink-rendering/api/restricted_current.txt +++ b/ink/ink-rendering/api/restricted_current.txt @@ -2,6 +2,7 @@ package androidx.ink.rendering.android.canvas { public interface CanvasStrokeRenderer { + method public static androidx.ink.rendering.android.canvas.CanvasStrokeRenderer create(); method public static androidx.ink.rendering.android.canvas.CanvasStrokeRenderer create(optional androidx.ink.brush.TextureBitmapStore textureStore); method public default void draw(android.graphics.Canvas canvas, androidx.ink.strokes.InProgressStroke inProgressStroke, android.graphics.Matrix strokeToScreenTransform); method public default void draw(android.graphics.Canvas canvas, androidx.ink.strokes.InProgressStroke inProgressStroke, androidx.ink.geometry.AffineTransform strokeToScreenTransform); @@ -11,6 +12,7 @@ package androidx.ink.rendering.android.canvas { } public static final class CanvasStrokeRenderer.Companion { + method public androidx.ink.rendering.android.canvas.CanvasStrokeRenderer create(); method public androidx.ink.rendering.android.canvas.CanvasStrokeRenderer create(optional androidx.ink.brush.TextureBitmapStore textureStore); method @BytecodeOnly public static androidx.ink.rendering.android.canvas.CanvasStrokeRenderer! create$default(androidx.ink.rendering.android.canvas.CanvasStrokeRenderer.Companion!, androidx.ink.brush.TextureBitmapStore!, int, Object!); } diff --git a/ink/ink-rendering/src/androidDeviceTest/kotlin/androidx/ink/rendering/android/canvas/StockBrushesConsistencyTest.kt b/ink/ink-rendering/src/androidDeviceTest/kotlin/androidx/ink/rendering/android/canvas/StockBrushesConsistencyTest.kt index 0aaf26e2854c4..dcbf013cfef08 100644 --- a/ink/ink-rendering/src/androidDeviceTest/kotlin/androidx/ink/rendering/android/canvas/StockBrushesConsistencyTest.kt +++ b/ink/ink-rendering/src/androidDeviceTest/kotlin/androidx/ink/rendering/android/canvas/StockBrushesConsistencyTest.kt @@ -49,7 +49,6 @@ class StockBrushesConsistencyTest() { StockBrushes.pressurePen(StockBrushes.PressurePenVersion.V1), StockBrushes.highlighter(version = StockBrushes.HighlighterVersion.V1), StockBrushes.dashedLine(StockBrushes.DashedLineVersion.V1), - StockBrushes.pencilUnstable, ) // Collections of test values to check consistency across. diff --git a/ink/ink-rendering/src/androidDeviceTest/kotlin/androidx/ink/rendering/android/canvas/StockBrushesTest.kt b/ink/ink-rendering/src/androidDeviceTest/kotlin/androidx/ink/rendering/android/canvas/StockBrushesTest.kt index 5cbd6ce5df1c8..22eb682fe3a02 100644 --- a/ink/ink-rendering/src/androidDeviceTest/kotlin/androidx/ink/rendering/android/canvas/StockBrushesTest.kt +++ b/ink/ink-rendering/src/androidDeviceTest/kotlin/androidx/ink/rendering/android/canvas/StockBrushesTest.kt @@ -26,6 +26,7 @@ import androidx.ink.brush.ExperimentalInkCustomBrushApi import androidx.ink.brush.InputToolType import androidx.ink.brush.StockBrushes import androidx.ink.strokes.ImmutableStrokeInputBatch +import androidx.ink.strokes.InProgressStroke import androidx.ink.strokes.MutableStrokeInputBatch import androidx.ink.strokes.Stroke import androidx.ink.strokes.StrokeInput @@ -71,7 +72,6 @@ class StockBrushesTest(val brushName: String) { .copy(clientBrushFamilyId = "highlighter_1"), StockBrushes.dashedLine(StockBrushes.DashedLineVersion.V1) .copy(clientBrushFamilyId = "dashed-line_1"), - StockBrushes.pencilUnstable.copy(clientBrushFamilyId = "pencil_1"), StockBrushes.emojiHighlighter( clientTextureId = "emoji_heart", showMiniEmojiTrail = true, @@ -325,6 +325,30 @@ class StockBrushesTest(val brushName: String) { ) } + @Test + fun emojiHighlighterHasCorrectMiniEmojiTrailBehavior() { + if ( + !(family.clientBrushFamilyId in + listOf("heart_emoji_highlighter_1", "heart_emoji_highlighter_no_trail_1")) + ) { + return + } + val stroke = + InProgressStroke().apply { + start(makeBrush(family = family, size = 10f)) + enqueueInputs(helper.octogonStylusInputs, ImmutableStrokeInputBatch.EMPTY) + updateShape( + helper.octogonStylusInputs + .get(helper.octogonStylusInputs.size - 1) + .elapsedTimeMillis + ) + } + assertStrokesMatchGolden( + listOf(listOf(listOf(stroke.toImmutable()))), + "emojiHighlighterHasCorrectMiniEmojiTrailBehavior_" + family.clientBrushFamilyId, + ) + } + /** * Creates a brush with the given specifications. * diff --git a/ink/ink-rendering/src/androidDeviceTest/kotlin/androidx/ink/rendering/android/canvas/StockBrushesTestHelper.kt b/ink/ink-rendering/src/androidDeviceTest/kotlin/androidx/ink/rendering/android/canvas/StockBrushesTestHelper.kt index 04ac829d2821d..577526ed682eb 100644 --- a/ink/ink-rendering/src/androidDeviceTest/kotlin/androidx/ink/rendering/android/canvas/StockBrushesTestHelper.kt +++ b/ink/ink-rendering/src/androidDeviceTest/kotlin/androidx/ink/rendering/android/canvas/StockBrushesTestHelper.kt @@ -23,7 +23,6 @@ import android.graphics.Matrix import androidx.core.graphics.withMatrix import androidx.ink.brush.ExperimentalInkCustomBrushApi import androidx.ink.brush.InputToolType -import androidx.ink.brush.StockBrushes import androidx.ink.brush.StockTextureBitmapStore import androidx.ink.geometry.BoxAccumulator import androidx.ink.rendering.test.R @@ -71,7 +70,6 @@ class StockBrushesTestHelper(private val context: Context) { private var bounds = BoxAccumulator() private val textureStore = StockTextureBitmapStore(resources).apply { - preloadStockBrushesTextures(StockBrushes.pencilUnstable) check( addTexture( "emoji_heart", diff --git a/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/CanvasStrokeRenderer.kt b/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/CanvasStrokeRenderer.kt index 0e2edd17b0e24..ccb85f6cc55c3 100644 --- a/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/CanvasStrokeRenderer.kt +++ b/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/CanvasStrokeRenderer.kt @@ -116,7 +116,7 @@ public interface CanvasStrokeRenderer { * blurry or aliased. */ @ExperimentalInkCustomBrushApi - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi public fun draw( canvas: Canvas, stroke: Stroke, @@ -158,7 +158,7 @@ public interface CanvasStrokeRenderer { * appear blurry or aliased. */ @ExperimentalInkCustomBrushApi - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi public fun draw( canvas: Canvas, stroke: Stroke, @@ -196,7 +196,7 @@ public interface CanvasStrokeRenderer { * blurry or aliased. */ @ExperimentalInkCustomBrushApi - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi public fun draw( canvas: Canvas, inProgressStroke: InProgressStroke, @@ -234,7 +234,7 @@ public interface CanvasStrokeRenderer { * appear blurry or aliased. */ @ExperimentalInkCustomBrushApi - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi public fun draw( canvas: Canvas, inProgressStroke: InProgressStroke, @@ -254,8 +254,8 @@ public interface CanvasStrokeRenderer { * @param textureStore The [TextureBitmapStore] that will be called to retrieve image data * for drawing textured strokes. */ - @Suppress("MissingJvmstatic") @JvmStatic + @JvmOverloads public fun create( textureStore: TextureBitmapStore = TextureBitmapStore { null } ): CanvasStrokeRenderer { diff --git a/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/internal/CanvasMeshRenderer.kt b/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/internal/CanvasMeshRenderer.kt index 1aec59bd3cf7a..c382c635a7ee4 100644 --- a/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/internal/CanvasMeshRenderer.kt +++ b/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/internal/CanvasMeshRenderer.kt @@ -861,9 +861,6 @@ internal class CanvasMeshRenderer( ) companion object { - init { - NativeLoader.load() - } private val SUPPORTED_SELF_OVERLAP_MODES = setOf(SelfOverlap.ANY, SelfOverlap.ACCUMULATE) diff --git a/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/internal/CanvasStrokeUnifiedRenderer.kt b/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/internal/CanvasStrokeUnifiedRenderer.kt index 2f58dfb1a1897..cbdfdca33ea45 100644 --- a/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/internal/CanvasStrokeUnifiedRenderer.kt +++ b/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/internal/CanvasStrokeUnifiedRenderer.kt @@ -20,7 +20,6 @@ import android.graphics.Canvas import android.graphics.Matrix import android.os.Build import android.util.Log -import androidx.annotation.RestrictTo import androidx.annotation.VisibleForTesting import androidx.ink.brush.Brush import androidx.ink.brush.ExperimentalInkCustomBrushApi @@ -62,7 +61,6 @@ internal class CanvasStrokeUnifiedRenderer( } @ExperimentalInkCustomBrushApi - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi override fun draw( canvas: Canvas, stroke: Stroke, @@ -74,7 +72,6 @@ internal class CanvasStrokeUnifiedRenderer( } @ExperimentalInkCustomBrushApi - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi override fun draw( canvas: Canvas, stroke: Stroke, @@ -125,7 +122,6 @@ internal class CanvasStrokeUnifiedRenderer( } @ExperimentalInkCustomBrushApi - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi override fun draw( canvas: Canvas, inProgressStroke: InProgressStroke, @@ -137,7 +133,6 @@ internal class CanvasStrokeUnifiedRenderer( } @ExperimentalInkCustomBrushApi - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi override fun draw( canvas: Canvas, inProgressStroke: InProgressStroke, diff --git a/ink/ink-storage/README.md b/ink/ink-storage/README.md new file mode 100644 index 0000000000000..c17cfc2fd63e8 --- /dev/null +++ b/ink/ink-storage/README.md @@ -0,0 +1,10 @@ +# Ink Storage Module + +Logic for compactly serializing and deserializing Ink brushes and strokes. The +at-rest format is gzip-compressed binary-protobuf. The underlying protobuf +messages use a compact representation for stroke geometry, and the message +format is defined in the `.proto` files published +[here](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:ink/ink-storage/src/commonMain/proto/). + +Currently, this supports storage of `BrushFamily` and `StrokeInputBatch` +objects. diff --git a/ink/ink-storage/src/commonMain/proto/brush_family.proto b/ink/ink-storage/src/commonMain/proto/brush_family.proto index a414a5cf9d4f8..8f6b760251d34 100644 --- a/ink/ink-storage/src/commonMain/proto/brush_family.proto +++ b/ink/ink-storage/src/commonMain/proto/brush_family.proto @@ -87,6 +87,12 @@ message BrushFamily { // A mapping of texture IDs (as used in `BrushPaint.TextureLayer`) to bitmaps // in PNG format. map texture_id_to_bitmap = 6; + + // A multi-line, human-readable string with a description of the brush and how + // it works, with the intended audience being designers/developers who are + // editing the brush definition. This string is not generally intended to be + // displayed to end users. + optional string developer_comment = 7; } // A `BrushCoat` represents one coat of ink applied by a brush. It includes a @@ -923,12 +929,12 @@ message BrushBehavior { // Dimensions/units for measuring the `damping_gap` field of a `DampingNode`. - enum DampingSource { - DAMPING_SOURCE_UNSPECIFIED = 0; + enum ProgressDomain { + PROGRESS_DOMAIN_UNSPECIFIED = 0; // Value damping occurs over time, and the `damping_gap` is measured in // seconds. - DAMPING_SOURCE_TIME_IN_SECONDS = 1; + PROGRESS_DOMAIN_TIME_IN_SECONDS = 1; // Value damping occurs over distance traveled by the input pointer, and the // `damping_gap` is measured in centimeters. If the input data does not @@ -936,11 +942,11 @@ message BrushBehavior { // (e.g. as may be the case for programmatically-generated inputs), then no // damping will be performed (i.e. the `damping_gap` will be treated as // zero). - DAMPING_SOURCE_DISTANCE_IN_CENTIMETERS = 2; + PROGRESS_DOMAIN_DISTANCE_IN_CENTIMETERS = 2; // Value damping occurs over distance traveled by the input pointer, and the // `damping_gap` is measured in multiples of the brush size. - DAMPING_SOURCE_DISTANCE_IN_MULTIPLES_OF_BRUSH_SIZE = 3; + PROGRESS_DOMAIN_DISTANCE_IN_MULTIPLES_OF_BRUSH_SIZE = 3; } @@ -976,6 +982,7 @@ message BrushBehavior { InterpolationNode interpolation_node = 9; NoiseNode noise_node = 10; PolarTargetNode polar_target_node = 11; + IntegralNode integral_node = 12; } } @@ -1028,13 +1035,13 @@ message BrushBehavior { // Output: The current random value. // // To be valid: - // * `vary_over` must be a valid `DampingSource` enumerator. + // * `vary_over` must be a valid `ProgressDomain` enumerator. // * `base_period` must be finite and strictly positive. message NoiseNode { optional fixed32 seed = 1; // The domain units over which random noise is generated. - optional DampingSource vary_over = 2; + optional ProgressDomain vary_over = 2; // The period (in `vary_over` units) over which the output value smoothly // varies from one random value to another uncorrelated random value. (In @@ -1085,11 +1092,11 @@ message BrushBehavior { // null, the output value is null until the first non-null input. // // To be valid: - // * `damping_source` must be a valid `DampingSource` enumerator. + // * `damping_source` must be a valid `ProgressDomain` enumerator. // * `damping_gap` must be finite and non-negative. message DampingNode { // The domain units over which damping is applied. - optional DampingSource damping_source = 1; + optional ProgressDomain damping_source = 1; // A scaling factor, in `damping_source` units, for the damping. Smaller // gaps result in less damping, so the output follows the input more @@ -1140,6 +1147,39 @@ message BrushBehavior { optional Interpolation interpolation = 1; } + // Value node for integrating an input value over time or distance. + // + // Inputs: 1 + // + // Output: The integral of the input value since the start of the stroke, + // after inverse-lerping from the specified value range and applying the + // specified out-of-range behavior. If the input value ever becomes null, this + // node acts as though the input value were still equal to its most recent + // non-null value. If the input value starts out null, it is treated as zero + // until the first non-null input. + // + // To be valid: + // * `integrate_over` must be a valid `ProgressDomain` enumerator. + // * The endpoints of `integral_value_range` must be finite and distinct. + // * `integral_out_of_range_behavior` must be a valid `OutOfRange` + // enumerator. + message IntegralNode { + // The variable and units (e.g. time or distance) over which the input value + // is integrated. + optional ProgressDomain integrate_over = 1; + + // The integral value that maps to 0.0 in the output. Below this value, + // `out_of_range_behavior` determines the output value. + optional float integral_value_range_start = 2; + + // The integral value that maps to 1.0 in the output. Above this value, + // `out_of_range_behavior` determines the output value. + optional float integral_value_range_end = 3; + + // What to do with integral values outside the integral value range. + optional OutOfRange integral_out_of_range_behavior = 4; + } + // Terminal node that consumes a single input value to modify a scalar brush // tip property. // @@ -1201,6 +1241,12 @@ message BrushBehavior { // Were fields controlling brush behavior, use `nodes` instead. reserved 1 to 14; + + // A multi-line, human-readable string with a description of this brush + // behavior and its purpose within the brush, with the intended audience being + // designers/developers who are editing the brush definition. This string is + // not generally intended to be displayed to end users. + optional string developer_comment = 16; } // Transforms the brush color to be used as an alternative base color for any diff --git a/ink/ink-storage/src/jvmAndAndroidMain/kotlin/androidx/ink/storage/DecompressedBytes.kt b/ink/ink-storage/src/jvmAndAndroidMain/kotlin/androidx/ink/storage/DecompressedBytes.kt index 135fd168303ac..9c9b70a910ff3 100644 --- a/ink/ink-storage/src/jvmAndAndroidMain/kotlin/androidx/ink/storage/DecompressedBytes.kt +++ b/ink/ink-storage/src/jvmAndAndroidMain/kotlin/androidx/ink/storage/DecompressedBytes.kt @@ -23,10 +23,10 @@ import java.util.zip.GZIPInputStream /** Gets decompressed [ByteArray] from an [InputStream] of GZIP-compressed bytes. */ internal class DecompressedBytes(compressedBytesInputStream: InputStream) { /** The first [size] bytes of this contain the decompressed bytes, the rest are zero. */ - public val bytes: ByteArray + val bytes: ByteArray /** The size of the initial portion of [bytes] containing the decompressed bytes. */ - public val size: Int + val size: Int init { var byteArray = ByteArray(DECOMPRESSED_BYTES_INITIAL_CAPACITY) @@ -59,7 +59,7 @@ internal class DecompressedBytes(compressedBytesInputStream: InputStream) { } } - public companion object { - public const val DECOMPRESSED_BYTES_INITIAL_CAPACITY: Int = 32 * 1024 + companion object { + const val DECOMPRESSED_BYTES_INITIAL_CAPACITY: Int = 32 * 1024 } } diff --git a/ink/ink-strokes/README.md b/ink/ink-strokes/README.md new file mode 100644 index 0000000000000..ab58a70f2ab1e --- /dev/null +++ b/ink/ink-strokes/README.md @@ -0,0 +1,6 @@ +# Ink Strokes Module + +This module provides logic for constructing and representing freehand strokes. +`InProgressStroke` is used to construct strokes and represent partial strokes +while pointer input is in progress. `Stroke` is used to represent finished +strokes. diff --git a/ink/ink-strokes/api/current.txt b/ink/ink-strokes/api/current.txt index fcf243aba5167..fad127fded90e 100644 --- a/ink/ink-strokes/api/current.txt +++ b/ink/ink-strokes/api/current.txt @@ -37,6 +37,7 @@ package androidx.ink.strokes { method public void start(androidx.ink.brush.Brush brush); method @SuppressCompatibility @androidx.ink.brush.ExperimentalInkCustomBrushApi public void start(androidx.ink.brush.Brush brush, int noiseSeed); method public androidx.ink.strokes.Stroke toImmutable(); + method public void updateShape(); method public void updateShape(optional long currentElapsedTimeMillis); method @BytecodeOnly public static void updateShape$default(androidx.ink.strokes.InProgressStroke!, long, int, Object!); property public androidx.ink.brush.Brush? brush; diff --git a/ink/ink-strokes/api/restricted_current.txt b/ink/ink-strokes/api/restricted_current.txt index fcf243aba5167..fad127fded90e 100644 --- a/ink/ink-strokes/api/restricted_current.txt +++ b/ink/ink-strokes/api/restricted_current.txt @@ -37,6 +37,7 @@ package androidx.ink.strokes { method public void start(androidx.ink.brush.Brush brush); method @SuppressCompatibility @androidx.ink.brush.ExperimentalInkCustomBrushApi public void start(androidx.ink.brush.Brush brush, int noiseSeed); method public androidx.ink.strokes.Stroke toImmutable(); + method public void updateShape(); method public void updateShape(optional long currentElapsedTimeMillis); method @BytecodeOnly public static void updateShape$default(androidx.ink.strokes.InProgressStroke!, long, int, Object!); property public androidx.ink.brush.Brush? brush; diff --git a/ink/ink-strokes/src/jvmAndAndroidMain/kotlin/androidx/ink/strokes/InProgressStroke.kt b/ink/ink-strokes/src/jvmAndAndroidMain/kotlin/androidx/ink/strokes/InProgressStroke.kt index e869700e11447..48519f6ec1d8e 100644 --- a/ink/ink-strokes/src/jvmAndAndroidMain/kotlin/androidx/ink/strokes/InProgressStroke.kt +++ b/ink/ink-strokes/src/jvmAndAndroidMain/kotlin/androidx/ink/strokes/InProgressStroke.kt @@ -176,7 +176,7 @@ public class InProgressStroke { * @throws [IllegalArgumentException] If [currentElapsedTimeMillis] is negative or decreased * from a previous call to this method for the same in-progress stroke. */ - @Suppress("MissingJvmstatic") + @JvmOverloads public fun updateShape(currentElapsedTimeMillis: Long = Long.MAX_VALUE) { val success = InProgressStrokeNative.updateShape(nativePointer, currentElapsedTimeMillis) check(success) { "Should have thrown an exception if updateShape failed." } @@ -363,12 +363,6 @@ public class InProgressStroke { } } - /** @see getOutlineCount */ - @IntRange(from = 0) - @Deprecated("Renamed to getOutlineCount") - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi - public fun outlineCount(@IntRange(from = 0) coatIndex: Int): Int = getOutlineCount(coatIndex) - /** * Returns the number of outline points for the specified outline and brush coat. * [populateOutlinePosition] must treat the result of this as the upper bound of its @@ -391,15 +385,6 @@ public class InProgressStroke { .also { check(it >= 0) } } - /** @see getOutlineVertexCount */ - @Deprecated("Renamed to getOutlineVertexCount") - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi - @IntRange(from = 0) - public fun outlineVertexCount( - @IntRange(from = 0) coatIndex: Int, - @IntRange(from = 0) outlineIndex: Int, - ): Int = getOutlineVertexCount(coatIndex, outlineIndex) - /** * Fills [outPosition] with the x and y coordinates of the specified outline vertex. * diff --git a/ink/ink-strokes/src/jvmAndAndroidMain/kotlin/androidx/ink/strokes/StrokeInputBatch.kt b/ink/ink-strokes/src/jvmAndAndroidMain/kotlin/androidx/ink/strokes/StrokeInputBatch.kt index 596a6712017d3..cbdc50c763fae 100644 --- a/ink/ink-strokes/src/jvmAndAndroidMain/kotlin/androidx/ink/strokes/StrokeInputBatch.kt +++ b/ink/ink-strokes/src/jvmAndAndroidMain/kotlin/androidx/ink/strokes/StrokeInputBatch.kt @@ -116,7 +116,7 @@ public abstract class StrokeInputBatch internal constructor(nativePointer: Long) return outStrokeInput } - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi public abstract fun toImmutable(): ImmutableStrokeInputBatch // NOMUTANTS -- Not tested post garbage collection. @@ -141,7 +141,7 @@ public abstract class StrokeInputBatch internal constructor(nativePointer: Long) public class ImmutableStrokeInputBatch private constructor(nativePointer: Long) : StrokeInputBatch(nativePointer) { - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi public override fun toImmutable(): ImmutableStrokeInputBatch = this public override fun toString(): String = "ImmutableStrokeInputBatch(size=$size)" @@ -334,7 +334,7 @@ public class MutableStrokeInputBatch : StrokeInputBatch(StrokeInputBatchNative.c MutableStrokeInputBatchNative.setNoiseSeed(nativePointer, seed) /** Create [ImmutableStrokeInputBatch] with the accumulated StrokeInputs. */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi public override fun toImmutable(): ImmutableStrokeInputBatch = @OptIn(ExperimentalInkCustomBrushApi::class) if (isEmpty() && getNoiseSeed() == 0) { diff --git a/ink/ink-strokes/src/jvmAndAndroidMain/kotlin/androidx/ink/strokes/testing/StrokeTestHelper.kt b/ink/ink-strokes/src/jvmAndAndroidMain/kotlin/androidx/ink/strokes/testing/StrokeTestHelper.kt index 894e08c2ea341..c67e1c6d32e00 100644 --- a/ink/ink-strokes/src/jvmAndAndroidMain/kotlin/androidx/ink/strokes/testing/StrokeTestHelper.kt +++ b/ink/ink-strokes/src/jvmAndAndroidMain/kotlin/androidx/ink/strokes/testing/StrokeTestHelper.kt @@ -29,7 +29,7 @@ import kotlin.jvm.JvmOverloads */ @JvmOverloads @VisibleForTesting -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public fun buildStrokeInputBatchFromPoints( points: FloatArray, toolType: InputToolType = InputToolType.STYLUS, From f03376208c9f757e23067247dcecc3c391e8db37 Mon Sep 17 00:00:00 2001 From: Leticia Santos Date: Thu, 5 Feb 2026 14:11:15 -0500 Subject: [PATCH 06/21] [Button] Make isTextButtonContentPaddingFixEnabled flag true by default. Test: existing Relnote: Update TextButtons to have correct material3 paddings specs. In order to opt out and revert to the old behavior you should set isTextButtonContentPaddingFixEnabled to false in your application. Change-Id: I66c8e7f63fd431ab067d388aec4fd436c30e697a --- .../kotlin/androidx/compose/material3/ButtonTest.kt | 9 +++------ .../androidx/compose/material3/ComposeMaterial3Flags.kt | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/compose/material3/material3/src/androidDeviceTest/kotlin/androidx/compose/material3/ButtonTest.kt b/compose/material3/material3/src/androidDeviceTest/kotlin/androidx/compose/material3/ButtonTest.kt index b6333eca4828c..87fbbf5a534c2 100644 --- a/compose/material3/material3/src/androidDeviceTest/kotlin/androidx/compose/material3/ButtonTest.kt +++ b/compose/material3/material3/src/androidDeviceTest/kotlin/androidx/compose/material3/ButtonTest.kt @@ -263,10 +263,8 @@ class ButtonTest { ) } - @OptIn(ExperimentalMaterial3Api::class) @Test - fun text_button_isTextButtonContentPaddingFixEnabled_positioning() { - ComposeMaterial3Flags.isTextButtonContentPaddingFixEnabled = true + fun text_button_positioning() { rule.setMaterialContent(lightColorScheme()) { TextButton( onClick = { /* Do something! */ }, @@ -293,10 +291,9 @@ class ButtonTest { ) } - @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) + @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Test - fun text_button_shapesRequired_isTextButtonContentPaddingFixEnabled_positioning() { - ComposeMaterial3Flags.isTextButtonContentPaddingFixEnabled = true + fun text_button_shapesRequired_positioning() { rule.setMaterialContent(lightColorScheme()) { TextButton( onClick = { /* Do something! */ }, diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ComposeMaterial3Flags.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ComposeMaterial3Flags.kt index 337221739c7bd..cd3f6d84f0849 100644 --- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ComposeMaterial3Flags.kt +++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ComposeMaterial3Flags.kt @@ -98,5 +98,5 @@ object ComposeMaterial3Flags { */ @field:Suppress("MutableBareField") @JvmField - var isTextButtonContentPaddingFixEnabled: Boolean = false + var isTextButtonContentPaddingFixEnabled: Boolean = true } From 5dae630cb2c5aa191c13098dfba1df67ea3095ce Mon Sep 17 00:00:00 2001 From: Kim Van Den Eeckhaut Date: Thu, 5 Feb 2026 19:48:49 +0000 Subject: [PATCH 07/21] Update docs-public/build.gradle for media3 1.10.0-alpha01 Test: presubmit Bug: 475512397 Change-Id: I3bb50c325f09391d5fb2d581731ffc4608366fea --- docs-public/build.gradle | 60 +++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/docs-public/build.gradle b/docs-public/build.gradle index 62458435ce06a..f566ceb94a6f4 100644 --- a/docs-public/build.gradle +++ b/docs-public/build.gradle @@ -253,35 +253,37 @@ dependencies { docs("androidx.loader:loader:1.1.0") docs("androidx.media:media:1.7.1") // androidx.media3 is not hosted in androidx - docsWithoutApiSince("androidx.media3:media3-cast:1.9.1") - docsWithoutApiSince("androidx.media3:media3-common:1.9.1") - docsWithoutApiSince("androidx.media3:media3-common-ktx:1.9.1") - docsWithoutApiSince("androidx.media3:media3-container:1.9.1") - docsWithoutApiSince("androidx.media3:media3-database:1.9.1") - docsWithoutApiSince("androidx.media3:media3-datasource:1.9.1") - docsWithoutApiSince("androidx.media3:media3-datasource-cronet:1.9.1") - docsWithoutApiSince("androidx.media3:media3-datasource-okhttp:1.9.1") - docsWithoutApiSince("androidx.media3:media3-datasource-rtmp:1.9.1") - docsWithoutApiSince("androidx.media3:media3-decoder:1.9.1") - docsWithoutApiSince("androidx.media3:media3-effect:1.9.1") - docsWithoutApiSince("androidx.media3:media3-exoplayer:1.9.1") - docsWithoutApiSince("androidx.media3:media3-exoplayer-dash:1.9.1") - docsWithoutApiSince("androidx.media3:media3-exoplayer-hls:1.9.1") - docsWithoutApiSince("androidx.media3:media3-exoplayer-ima:1.9.1") - docsWithoutApiSince("androidx.media3:media3-exoplayer-rtsp:1.9.1") - docsWithoutApiSince("androidx.media3:media3-exoplayer-smoothstreaming:1.9.1") - docsWithoutApiSince("androidx.media3:media3-exoplayer-workmanager:1.9.1") - docsWithoutApiSince("androidx.media3:media3-extractor:1.9.1") - docsWithoutApiSince("androidx.media3:media3-inspector:1.9.1") - docsWithoutApiSince("androidx.media3:media3-muxer:1.9.1") - docsWithoutApiSince("androidx.media3:media3-session:1.9.1") - docsWithoutApiSince("androidx.media3:media3-test-utils:1.9.1") - docsWithoutApiSince("androidx.media3:media3-test-utils-robolectric:1.9.1") - docsWithoutApiSince("androidx.media3:media3-transformer:1.9.1") - docsWithoutApiSince("androidx.media3:media3-ui:1.9.1") - docsWithoutApiSince("androidx.media3:media3-ui-compose:1.9.1") - docsWithoutApiSince("androidx.media3:media3-ui-compose-material3:1.9.1") - docsWithoutApiSince("androidx.media3:media3-ui-leanback:1.9.1") + docsWithoutApiSince("androidx.media3:media3-cast:1.10.0-alpha01") + docsWithoutApiSince("androidx.media3:media3-common:1.10.0-alpha01") + docsWithoutApiSince("androidx.media3:media3-common-ktx:1.10.0-alpha01") + docsWithoutApiSince("androidx.media3:media3-container:1.10.0-alpha01") + docsWithoutApiSince("androidx.media3:media3-database:1.10.0-alpha01") + docsWithoutApiSince("androidx.media3:media3-datasource:1.10.0-alpha01") + docsWithoutApiSince("androidx.media3:media3-datasource-cronet:1.10.0-alpha01") + docsWithoutApiSince("androidx.media3:media3-datasource-okhttp:1.10.0-alpha01") + docsWithoutApiSince("androidx.media3:media3-datasource-rtmp:1.10.0-alpha01") + docsWithoutApiSince("androidx.media3:media3-decoder:1.10.0-alpha01") + docsWithoutApiSince("androidx.media3:media3-effect:1.10.0-alpha01") + docsWithoutApiSince("androidx.media3:media3-effect-lottie:1.10.0-alpha01") + docsWithoutApiSince("androidx.media3:media3-exoplayer:1.10.0-alpha01") + docsWithoutApiSince("androidx.media3:media3-exoplayer-dash:1.10.0-alpha01") + docsWithoutApiSince("androidx.media3:media3-exoplayer-hls:1.10.0-alpha01") + docsWithoutApiSince("androidx.media3:media3-exoplayer-ima:1.10.0-alpha01") + docsWithoutApiSince("androidx.media3:media3-exoplayer-rtsp:1.10.0-alpha01") + docsWithoutApiSince("androidx.media3:media3-exoplayer-smoothstreaming:1.10.0-alpha01") + docsWithoutApiSince("androidx.media3:media3-exoplayer-workmanager:1.10.0-alpha01") + docsWithoutApiSince("androidx.media3:media3-extractor:1.10.0-alpha01") + docsWithoutApiSince("androidx.media3:media3-inspector:1.10.0-alpha01") + docsWithoutApiSince("androidx.media3:media3-inspector-frame:1.10.0-alpha01") + docsWithoutApiSince("androidx.media3:media3-muxer:1.10.0-alpha01") + docsWithoutApiSince("androidx.media3:media3-session:1.10.0-alpha01") + docsWithoutApiSince("androidx.media3:media3-test-utils:1.10.0-alpha01") + docsWithoutApiSince("androidx.media3:media3-test-utils-robolectric:1.10.0-alpha01") + docsWithoutApiSince("androidx.media3:media3-transformer:1.10.0-alpha01") + docsWithoutApiSince("androidx.media3:media3-ui:1.10.0-alpha01") + docsWithoutApiSince("androidx.media3:media3-ui-compose:1.10.0-alpha01") + docsWithoutApiSince("androidx.media3:media3-ui-compose-material3:1.10.0-alpha01") + docsWithoutApiSince("androidx.media3:media3-ui-leanback:1.10.0-alpha01") docs("androidx.mediarouter:mediarouter:1.8.1") docs("androidx.mediarouter:mediarouter-testing:1.8.1") docs("androidx.metrics:metrics-performance:1.0.0") From 218bd45e88bbda87691d19277d9efc9a9010bcf5 Mon Sep 17 00:00:00 2001 From: Derek Xu Date: Thu, 5 Feb 2026 15:48:56 -0500 Subject: [PATCH 08/21] Use `advanceUntilIdle` instead of `runCurrent` in `SnapshotFlowBenchmark` Test: This CL modifies a test Change-Id: I49c2afe8622c8acbb789c2523c94eb3d7de51145 --- .../compose/runtime/benchmark/SnapshotFlowBenchmark.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/SnapshotFlowBenchmark.kt b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/SnapshotFlowBenchmark.kt index 0bf767c7b5aa1..8e3cee25bd623 100644 --- a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/SnapshotFlowBenchmark.kt +++ b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/SnapshotFlowBenchmark.kt @@ -90,7 +90,10 @@ class SnapshotFlowBenchmark( // This test uses a `runTest` single-threaded dispatcher, which means that changes // aren't flushed to `snapshotFlow`s until we `yield()` intentionally. - runWithMeasurementDisabled { testScheduler.runCurrent() } + runWithMeasurementDisabled { + testScheduler.advanceUntilIdle() + assertEquals(n, count) + } stateObjects.forEach { runWithMeasurementDisabled { it.value = true } @@ -146,7 +149,10 @@ class SnapshotFlowBenchmark( // This test uses a `runTest` single-threaded dispatcher, which means that changes // aren't flushed to `snapshotFlow`s until we `yield()` intentionally. - runWithMeasurementDisabled { testScheduler.runCurrent() } + runWithMeasurementDisabled { + testScheduler.advanceUntilIdle() + assertEquals(n, count) + } stateObjects.forEach { runWithMeasurementDisabled { it.value = true } From 96e88b6c5a2786a89d86ca95b329c899eaa770f2 Mon Sep 17 00:00:00 2001 From: George Mount Date: Fri, 30 Jan 2026 10:52:10 -0800 Subject: [PATCH 09/21] Remove ComposeUiFlags.isRectManagerOffsetUsageFromLayoutCoordinatesEnabled Fixes: 455601894 Relnote: "ComposeUiFlags.isRectManagerOffsetUsageFromLayoutCoordinatesEnabled was removed" Test: none Change-Id: I6e14a1bf7267e9ea61169a2ce556f53afad2ffd2 --- compose/ui/ui/api/current.txt | 2 -- compose/ui/ui/api/restricted_current.txt | 2 -- .../androidx/compose/ui/ComposeUiFlags.kt | 9 -------- .../compose/ui/node/NodeCoordinator.kt | 21 ++++++++----------- 4 files changed, 9 insertions(+), 25 deletions(-) diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt index a49c126b80e06..c82e6a28065a2 100644 --- a/compose/ui/ui/api/current.txt +++ b/compose/ui/ui/api/current.txt @@ -158,7 +158,6 @@ package androidx.compose.ui { property public boolean isIndirectPointerNavigationGestureDetectorEnabled; property public boolean isInitialFocusOnFocusableAvailable; property public boolean isOptimizedFocusEventDispatchEnabled; - property public boolean isRectManagerOffsetUsageFromLayoutCoordinatesEnabled; property public boolean isScrollCaptureCenteringEnabled; property public boolean isTrackpadGestureHandlingEnabled; property public boolean isTraversableDelegatesFixEnabled; @@ -172,7 +171,6 @@ package androidx.compose.ui { field public static boolean isIndirectPointerNavigationGestureDetectorEnabled; field public static boolean isInitialFocusOnFocusableAvailable; field public static boolean isOptimizedFocusEventDispatchEnabled; - field public static boolean isRectManagerOffsetUsageFromLayoutCoordinatesEnabled; field public static boolean isScrollCaptureCenteringEnabled; field public static boolean isTrackpadGestureHandlingEnabled; field public static boolean isTraversableDelegatesFixEnabled; diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt index c9e925abda20a..b92f12aefa5c3 100644 --- a/compose/ui/ui/api/restricted_current.txt +++ b/compose/ui/ui/api/restricted_current.txt @@ -158,7 +158,6 @@ package androidx.compose.ui { property public boolean isIndirectPointerNavigationGestureDetectorEnabled; property public boolean isInitialFocusOnFocusableAvailable; property public boolean isOptimizedFocusEventDispatchEnabled; - property public boolean isRectManagerOffsetUsageFromLayoutCoordinatesEnabled; property public boolean isScrollCaptureCenteringEnabled; property public boolean isTrackpadGestureHandlingEnabled; property public boolean isTraversableDelegatesFixEnabled; @@ -172,7 +171,6 @@ package androidx.compose.ui { field public static boolean isIndirectPointerNavigationGestureDetectorEnabled; field public static boolean isInitialFocusOnFocusableAvailable; field public static boolean isOptimizedFocusEventDispatchEnabled; - field public static boolean isRectManagerOffsetUsageFromLayoutCoordinatesEnabled; field public static boolean isScrollCaptureCenteringEnabled; field public static boolean isTrackpadGestureHandlingEnabled; field public static boolean isTraversableDelegatesFixEnabled; 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 index aefe6eaa5f502..203f28867bad0 100644 --- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposeUiFlags.kt +++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposeUiFlags.kt @@ -128,15 +128,6 @@ object ComposeUiFlags { @JvmField var isScrollCaptureCenteringEnabled: Boolean = true - /** - * Enable performance optimization where coordinates calculations like - * [androidx.compose.ui.layout.LayoutCoordinates.localToRoot] are using the cached offsets we - * already have in RectManager, instead of traversing the whole tree on each call. - */ - @field:Suppress("MutableBareField") - @JvmField - var isRectManagerOffsetUsageFromLayoutCoordinatesEnabled: Boolean = true - /** * Enables a fix where [TraversableNode] traversal method [findNearestAncestor] will take into * consideration any delegates that might also be traversable. 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 e3484482443fc..a7e5288a70749 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 @@ -21,7 +21,6 @@ import androidx.collection.MutableScatterSet import androidx.collection.mutableObjectIntMapOf import androidx.collection.mutableScatterSetOf import androidx.compose.runtime.snapshots.Snapshot -import androidx.compose.ui.ComposeUiFlags import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.FrameRateCategory import androidx.compose.ui.Modifier @@ -1153,17 +1152,15 @@ internal abstract class NodeCoordinator(override val layoutNode: LayoutNode) : var coordinator: NodeCoordinator? = this var position = relativeToLocal while (coordinator != null) { - if (ComposeUiFlags.isRectManagerOffsetUsageFromLayoutCoordinatesEnabled) { - val layoutNode = coordinator.layoutNode - if ( - coordinator === layoutNode.outerCoordinator && - !layoutNode.hasPositionalLayerTransformationsInOffsetFromRoot - ) { - val offsetFromRectList = - layoutNode.requireOwner().rectManager.getOffsetFromRectListFor(layoutNode) - if (offsetFromRectList != IntOffset.Max) { - return position + offsetFromRectList - } + val layoutNode = coordinator.layoutNode + if ( + coordinator === layoutNode.outerCoordinator && + !layoutNode.hasPositionalLayerTransformationsInOffsetFromRoot + ) { + val offsetFromRectList = + layoutNode.requireOwner().rectManager.getOffsetFromRectListFor(layoutNode) + if (offsetFromRectList != IntOffset.Max) { + return position + offsetFromRectList } } position = coordinator.toParentPosition(position) From 5ff98bcc3968f20daf60eaf27a3b17461514ad4b Mon Sep 17 00:00:00 2001 From: Brenton Bade Date: Thu, 5 Feb 2026 14:14:56 -0800 Subject: [PATCH 10/21] [glance] Add component tests for Buttons Add tests for hasAnyDescendent Test: Added Change-Id: I3ba5bc1e5350d33b5280c599d84414a05367be10 --- ...sGlanceTest.kt => GlanceComponentsTest.kt} | 101 +++++++++++++++++- 1 file changed, 99 insertions(+), 2 deletions(-) rename glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/componenttests/{ButtonsGlanceTest.kt => GlanceComponentsTest.kt} (51%) diff --git a/glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/componenttests/ButtonsGlanceTest.kt b/glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/componenttests/GlanceComponentsTest.kt similarity index 51% rename from glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/componenttests/ButtonsGlanceTest.kt rename to glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/componenttests/GlanceComponentsTest.kt index 94d1af8ba7c13..2dff23ae6de89 100644 --- a/glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/componenttests/ButtonsGlanceTest.kt +++ b/glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/componenttests/GlanceComponentsTest.kt @@ -16,34 +16,56 @@ package androidx.glance.appwidget.testing.unit.componenttests +import android.content.Intent import android.net.Uri import androidx.compose.runtime.Composable import androidx.glance.Button import androidx.glance.GlanceModifier +import androidx.glance.action.Action import androidx.glance.appwidget.ImageProvider +import androidx.glance.appwidget.action.actionStartActivity import androidx.glance.appwidget.components.CircleIconButton import androidx.glance.appwidget.components.FilledButton import androidx.glance.appwidget.components.SquareIconButton +import androidx.glance.appwidget.testing.unit.hasStartActivityClickAction import androidx.glance.appwidget.testing.unit.runGlanceAppWidgetUnitTest import androidx.glance.layout.Column import androidx.glance.semantics.semantics import androidx.glance.semantics.testTag import androidx.glance.testing.unit.assertHasClickAction +import androidx.glance.testing.unit.hasAnyDescendant import androidx.glance.testing.unit.hasTestTag import androidx.glance.testing.unit.hasText +import androidx.glance.text.Text import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config private const val arbitraryText = "some text" private const val buttonTestTag = "button-under-test" +private const val columnTestTag = "column-under-test" private val anImageProvider = ImageProvider(uri = Uri.parse("example.com")) private const val contentDescription = "a content description" +/** + * Tests that the unit testing framework runs correctly on Glance's components. The intent is that + * changes to the components should not break existing unit tests. + * + * TODO: add more tests for more components 480199909. Also, for each button test case, ensure that + * the behavior is tested for the base button, TextButton, and IconButton + */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [Config.TARGET_SDK]) class ButtonsGlanceTest { + /** + * Test that the M3 buttons and the basic button behave in the same way. They have different + * implementations internally. + */ @Test fun translateButton_hasCorrectText() = runGlanceAppWidgetUnitTest { - // Set the composable to test val buttonText = "Button Text" val m3ButtonText = "M3 Btn Text" provideComposable { @@ -53,7 +75,6 @@ class ButtonsGlanceTest { } } - // Perform assertions onNode(hasText(m3ButtonText)).assertExists() onNode(hasText(buttonText)).assertExists() } @@ -82,6 +103,59 @@ class ButtonsGlanceTest { provideComposable { TestSquareIconButton() } onNode(hasTestTag(buttonTestTag)).assertHasClickAction() } + + @Test + fun m3FilledButton_hasStartActivityClickAction() = runGlanceAppWidgetUnitTest { + val (action, intent) = startActivityAction() + + provideComposable { TestStartActivityButton(action) } + + onNode(hasText(arbitraryText)).assertExists() + onAllNodes((hasText(arbitraryText))) + .filter(hasStartActivityClickAction(intent)) + .assertCountEquals(1) + } + + @Test + fun m3IconButton_hasStartActivityClickAction() = runGlanceAppWidgetUnitTest { + val (action, intent) = startActivityAction() + provideComposable { TestStartActivityIconButton(action) } + + onAllNodes(hasTestTag(buttonTestTag)) + .filter(hasStartActivityClickAction(intent)) + .assertCountEquals(1) + } + + @Test + fun hasAnyDescendent_columnAndText() = runGlanceAppWidgetUnitTest { + provideComposable { + Column(GlanceModifier.semantics { testTag = columnTestTag }) { Text(arbitraryText) } + } + + onAllNodes(hasAnyDescendant(hasText(arbitraryText))) + .filter(hasTestTag(columnTestTag)) + .assertCountEquals(1) + } + + @Test + fun hasAnyDescendent_columnAndM3FilledButton() = runGlanceAppWidgetUnitTest { + val (action, intent) = startActivityAction() + + provideComposable { + Column(GlanceModifier.semantics { testTag = columnTestTag }) { + FilledButton(arbitraryText, onClick = action) + } + } + + onAllNodes(hasAnyDescendant(hasText(arbitraryText))) + .filter(hasTestTag(columnTestTag)) + .assertCountEquals(1) + } +} + +private fun startActivityAction(): Pair { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://www.example.com")) + return actionStartActivity(intent = intent) to intent } @Composable @@ -109,3 +183,26 @@ private fun TestSquareIconButton() = onClick = {}, modifier = GlanceModifier.semantics { testTag = buttonTestTag }, ) + +@Composable +private fun TestStartActivityButton(startActivityAction: Action) { + Column { + FilledButton( + arbitraryText, + onClick = startActivityAction, + modifier = GlanceModifier.semantics { testTag = buttonTestTag }, + ) + } +} + +@Composable +private fun TestStartActivityIconButton(startActivityAction: Action) { + Column { + CircleIconButton( + imageProvider = anImageProvider, + contentDescription = contentDescription, + onClick = startActivityAction, + modifier = GlanceModifier.semantics { testTag = buttonTestTag }, + ) + } +} From bbdb2309e0b8edb3048053a35f11bd52e3c14a05 Mon Sep 17 00:00:00 2001 From: rahulkanojia Date: Wed, 4 Feb 2026 17:22:19 +0530 Subject: [PATCH 11/21] Relnote: Expose PdfDocument from PdfViewerFragment via onDocumentLoadSuccess(pdfDocument) Context: - Whenever a documentUri is set on PdfViewerFragment, the PdfLoader starts processing the supplied document and generates an instance of PdfDocument if the load was successful. Change: This CL provides access to the PdfDocument instance by introducing an overridable onLoadDocumentSuccess(PdfDocument) callback in PdfViewerFragment. This allows subclasses to get the metadata, document info, etc. It also deprecates existing `onLoadDocumentSuccess()` in favour of a more specific and useful callback. Bug: b/481616017 Test: ./gradlew :pdf:pdf-viewer-fragment:connectedAndroidTest Change-Id: I4b47d0634e2b16aaff6f1d39cfa3b0eb7a13f2c7 --- .../androidx/pdf/TestPdfViewerFragment.kt | 5 +---- .../testapp/ui/v2/PdfViewerFragmentExtended.kt | 4 ++-- pdf/pdf-viewer-fragment/api/current.txt | 3 ++- .../api/restricted_current.txt | 3 ++- .../pdf/viewer/TestPdfViewerFragment.kt | 3 ++- .../pdf/viewer/fragment/PdfViewerFragment.kt | 17 +++++++++++++---- 6 files changed, 22 insertions(+), 13 deletions(-) diff --git a/pdf/integration-tests/testapp/src/androidTest/kotlin/androidx/pdf/TestPdfViewerFragment.kt b/pdf/integration-tests/testapp/src/androidTest/kotlin/androidx/pdf/TestPdfViewerFragment.kt index fdc22514259aa..2c3c5c7e3691e 100644 --- a/pdf/integration-tests/testapp/src/androidTest/kotlin/androidx/pdf/TestPdfViewerFragment.kt +++ b/pdf/integration-tests/testapp/src/androidTest/kotlin/androidx/pdf/TestPdfViewerFragment.kt @@ -186,12 +186,9 @@ internal class TestPdfViewerFragment : PdfViewerFragment { } } - override fun onLoadDocumentSuccess() { + override fun onLoadDocumentSuccess(document: PdfDocument) { documentLoaded = true pdfLoadingIdlingResource.decrement() - } - - override fun onLoadDocumentSuccess(document: PdfDocument) { pdfDocument = document } diff --git a/pdf/integration-tests/testapp/src/main/kotlin/androidx/pdf/testapp/ui/v2/PdfViewerFragmentExtended.kt b/pdf/integration-tests/testapp/src/main/kotlin/androidx/pdf/testapp/ui/v2/PdfViewerFragmentExtended.kt index cad296c46dfe8..66cbc02b60db9 100644 --- a/pdf/integration-tests/testapp/src/main/kotlin/androidx/pdf/testapp/ui/v2/PdfViewerFragmentExtended.kt +++ b/pdf/integration-tests/testapp/src/main/kotlin/androidx/pdf/testapp/ui/v2/PdfViewerFragmentExtended.kt @@ -135,8 +135,8 @@ class PdfViewerFragmentExtended : PdfViewerFragment(), FeatureFlagListener { pdfThumbnailRecyclerView.visibility = View.GONE } - override fun onLoadDocumentSuccess() { - super.onLoadDocumentSuccess() + override fun onLoadDocumentSuccess(document: PdfDocument) { + super.onLoadDocumentSuccess(document) if (PdfFeatureFlags.isThumbnailPreviewEnabled) { thumbnailAdapter.clearThumbnails() generateThumbnails() diff --git a/pdf/pdf-viewer-fragment/api/current.txt b/pdf/pdf-viewer-fragment/api/current.txt index 1bfd4c88fa1b6..f51e1405f5888 100644 --- a/pdf/pdf-viewer-fragment/api/current.txt +++ b/pdf/pdf-viewer-fragment/api/current.txt @@ -16,7 +16,8 @@ package androidx.pdf.viewer.fragment { method public static final androidx.pdf.viewer.fragment.PdfViewerFragment newInstance(androidx.pdf.viewer.fragment.PdfStylingOptions pdfStylingOptions); method protected boolean onLinkClicked(androidx.pdf.content.ExternalLink externalLink); method public void onLoadDocumentError(Throwable error); - method public void onLoadDocumentSuccess(); + method @Deprecated public void onLoadDocumentSuccess(); + method public void onLoadDocumentSuccess(androidx.pdf.PdfDocument document); method @SuppressCompatibility @androidx.pdf.ExperimentalPdfApi public void onPdfViewCreated(androidx.pdf.view.PdfView pdfView); method public void onRequestImmersiveMode(boolean enterImmersive); method @InaccessibleFromKotlin public final void setDocumentUri(android.net.Uri?); diff --git a/pdf/pdf-viewer-fragment/api/restricted_current.txt b/pdf/pdf-viewer-fragment/api/restricted_current.txt index 1bfd4c88fa1b6..f51e1405f5888 100644 --- a/pdf/pdf-viewer-fragment/api/restricted_current.txt +++ b/pdf/pdf-viewer-fragment/api/restricted_current.txt @@ -16,7 +16,8 @@ package androidx.pdf.viewer.fragment { method public static final androidx.pdf.viewer.fragment.PdfViewerFragment newInstance(androidx.pdf.viewer.fragment.PdfStylingOptions pdfStylingOptions); method protected boolean onLinkClicked(androidx.pdf.content.ExternalLink externalLink); method public void onLoadDocumentError(Throwable error); - method public void onLoadDocumentSuccess(); + method @Deprecated public void onLoadDocumentSuccess(); + method public void onLoadDocumentSuccess(androidx.pdf.PdfDocument document); method @SuppressCompatibility @androidx.pdf.ExperimentalPdfApi public void onPdfViewCreated(androidx.pdf.view.PdfView pdfView); method public void onRequestImmersiveMode(boolean enterImmersive); method @InaccessibleFromKotlin public final void setDocumentUri(android.net.Uri?); diff --git a/pdf/pdf-viewer-fragment/src/androidTest/kotlin/androidx/pdf/viewer/TestPdfViewerFragment.kt b/pdf/pdf-viewer-fragment/src/androidTest/kotlin/androidx/pdf/viewer/TestPdfViewerFragment.kt index f02a7740f1b17..58dc795e85149 100644 --- a/pdf/pdf-viewer-fragment/src/androidTest/kotlin/androidx/pdf/viewer/TestPdfViewerFragment.kt +++ b/pdf/pdf-viewer-fragment/src/androidTest/kotlin/androidx/pdf/viewer/TestPdfViewerFragment.kt @@ -27,6 +27,7 @@ import androidx.annotation.RequiresExtension import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat +import androidx.pdf.PdfDocument import androidx.pdf.view.PdfView import androidx.pdf.viewer.fragment.PdfStylingOptions import androidx.pdf.viewer.fragment.PdfViewerFragment @@ -146,7 +147,7 @@ internal class TestPdfViewerFragment : PdfViewerFragment { } } - override fun onLoadDocumentSuccess() { + override fun onLoadDocumentSuccess(document: PdfDocument) { documentLoaded = true pdfLoadingIdlingResource.decrement() pdfPagesFullyRenderedIdlingResource.startPolling() diff --git a/pdf/pdf-viewer-fragment/src/main/kotlin/androidx/pdf/viewer/fragment/PdfViewerFragment.kt b/pdf/pdf-viewer-fragment/src/main/kotlin/androidx/pdf/viewer/fragment/PdfViewerFragment.kt index ba9405d1d89a0..c4f70990ca8ce 100644 --- a/pdf/pdf-viewer-fragment/src/main/kotlin/androidx/pdf/viewer/fragment/PdfViewerFragment.kt +++ b/pdf/pdf-viewer-fragment/src/main/kotlin/androidx/pdf/viewer/fragment/PdfViewerFragment.kt @@ -220,17 +220,26 @@ public open class PdfViewerFragment constructor() : Fragment() { * destroyed, i.e., after [onCreate] has fully run and before [onDestroy] runs, and only on the * main thread. */ + @Deprecated( + message = + "Use onLoadDocumentSuccess(PdfDocument) to directly access the loaded document instance." + ) public open fun onLoadDocumentSuccess() {} /** - * Called when the document has been parsed and processed. + * Invoked when the document has been fully loaded and processed. * *

Note that this callback is dispatched only when the fragment is fully created and not yet * destroyed, i.e., after [onCreate] has fully run and before [onDestroy] runs, and only on the * main thread. + * + * @param document The [PdfDocument] instance representing the loaded PDF content. This + * reference will be valid till a new [documentUri] is set or the fragment is destroyed. */ - @RestrictTo(RestrictTo.Scope.LIBRARY) - protected open fun onLoadDocumentSuccess(document: PdfDocument) {} + public open fun onLoadDocumentSuccess(document: PdfDocument) { + // Trigger the deprecated parameterless callback to maintain backward compatibility + @Suppress("DEPRECATION") onLoadDocumentSuccess() + } /** * Invoked when a problem arises during the loading process of the PDF document. This callback @@ -784,7 +793,7 @@ public open class PdfViewerFragment constructor() : Fragment() { private fun handleDocumentLoaded(uiState: DocumentLoaded) { dismissPasswordDialog() onLoadDocumentSuccess(uiState.pdfDocument) - onLoadDocumentSuccess() + _pdfView.pdfDocument = uiState.pdfDocument _toolboxView.setPdfDocument(uiState.pdfDocument) setAnnotationIntentResolvability(uiState.pdfDocument.uri) From 82501eada57c5ab643498c0fe5318a711602bfe3 Mon Sep 17 00:00:00 2001 From: Omar Ismail Date: Fri, 6 Feb 2026 09:21:22 +0000 Subject: [PATCH 12/21] Use NDK lld linker for Android targets for 16KB alignment The default NDK linker used by Kotlin Natvie (LLD 8.0.7) is too old to support the `-z common-page-size`, which is required for proper 16KB page alignemnt without disabling RELRO. The Android NDK support in Kotlin Native is not well maintained: https://kotlinlang.org/docs/native-target-support.html#tier-3 This change configures KonanBuildService to use the LLD binary from the platform prebuilts (LLD 18+). The new linker supports the flag, resolving the "RELOR is not suffix" verification error. TESTED: created a test APK from the datastore-core lib and when I opened it in the APK analyzer, it did not show the verification error. BUG: 476745201 Change-Id: I1a525524a2c1100e87de440a3973b31d9501f975 --- .../androidx/build/clang/KonanBuildService.kt | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/buildSrc/private/src/main/kotlin/androidx/build/clang/KonanBuildService.kt b/buildSrc/private/src/main/kotlin/androidx/build/clang/KonanBuildService.kt index f7125b647a394..7654f717e468e 100644 --- a/buildSrc/private/src/main/kotlin/androidx/build/clang/KonanBuildService.kt +++ b/buildSrc/private/src/main/kotlin/androidx/build/clang/KonanBuildService.kt @@ -17,15 +17,19 @@ package androidx.build.clang import androidx.build.KonanPrebuiltsSetup +import androidx.build.OperatingSystem import androidx.build.ProjectLayoutType import androidx.build.clang.KonanBuildService.Companion.obtain import androidx.build.getKonanPrebuiltsFolder +import androidx.build.getOperatingSystem +import androidx.build.getSdkPath import java.io.ByteArrayOutputStream import javax.inject.Inject import org.gradle.api.GradleException import org.gradle.api.Project import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.FileCollection +import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.Property import org.gradle.api.provider.Provider import org.gradle.api.services.BuildService @@ -133,7 +137,18 @@ abstract class KonanBuildService @Inject constructor(private val execOperations: val linkerFlags = parameters.linkerArgs.get() + if (parameters.konanTarget.get().asKonanTarget.family == Family.ANDROID) { - listOf("-fuse-ld=lld", "-z", "max-page-size=16384") + val customLld = this@KonanBuildService.parameters.androidLldPath.orNull?.asFile + if (customLld != null) { + listOf( + "-fuse-ld=${customLld.absolutePath}", + "-z", + "max-page-size=16384", + "-z", + "common-page-size=16384", + ) + } else { + listOf("-fuse-ld=lld", "-z", "max-page-size=16384") + } } else { emptyList() } @@ -235,6 +250,18 @@ abstract class KonanBuildService @Inject constructor(private val execOperations: /** Location if konan prebuilts. Can be null if this is a playground project */ @get:Optional val prebuilts: DirectoryProperty + /** + * Use NDK lld linker in platform prebuilts for Android targets for 16KB alignment + * + * The default NDK linker used by Kotlin Native (LLD 8.0.7) is too old to support the `-z + * common-page-size` flag, which is required for proper 16KB page alignment without + * disabling RELRO (See b/476745201). + * + * The Android NDK support in Kotlin Native is not well maintained: + * https://kotlinlang.org/docs/native-target-support.html#tier-3 + */ + @get:Optional val androidLldPath: RegularFileProperty + /** * The type of the project (Playground vs AOSP main). This value is used to ensure we * initialize Konan distribution properly. @@ -263,6 +290,20 @@ abstract class KonanBuildService @Inject constructor(private val execOperations: it.parameters.projectLayoutType.set(ProjectLayoutType.from(project)) if (!ProjectLayoutType.isPlayground(project)) { it.parameters.prebuilts.set(project.getKonanPrebuiltsFolder()) + + val os = getOperatingSystem() + if (os == OperatingSystem.MAC || os == OperatingSystem.LINUX) { + val platform = if (os == OperatingSystem.MAC) "darwin" else "linux" + val lldPath = + project + .getSdkPath() + .resolve( + "ndk-bundle/toolchains/llvm/prebuilt/${platform}-x86_64/bin/ld.lld" + ) + if (lldPath.exists()) { + it.parameters.androidLldPath.set(lldPath) + } + } } } } From fbc1a70924291de0108dddabae09122db1ef3356 Mon Sep 17 00:00:00 2001 From: Omar Ismail Date: Fri, 6 Feb 2026 12:03:09 +0000 Subject: [PATCH 13/21] Enable ELF alignment verification in Android libs in KMP projects BUG: 476745201 Change-Id: Ibba68b5d08f9bde6c49840f4dccc8fa68c964575 --- .../androidx/build/AndroidXImplPlugin.kt | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt index c54cfeef94bee..3980e72be37de 100644 --- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt +++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt @@ -715,6 +715,7 @@ abstract class AndroidXImplPlugin @Inject constructor() : Plugin { ) project.configurePublicResourcesStub(variant) project.configureMultiplatformSourcesForAndroid(androidXExtension.samplesProjects) + project.configureVerifyELFRegionAlignment(variant) variant.aarMetadata.configureMinAgpVersion() } @@ -875,17 +876,7 @@ abstract class AndroidXImplPlugin @Inject constructor() : Plugin { taskProvider.configure { task -> task.dependsOn("compileReleaseJavaWithJavac") } } } - val verifyELFRegionAlignmentTaskProvider = - project.tasks.register( - variant.name + "VerifyELFRegionAlignment", - VerifyELFRegionAlignmentTask::class.java, - ) { task -> - task.mergedNativeLibs.set( - variant.artifacts.get(SingleArtifact.MERGED_NATIVE_LIBS) - ) - task.cacheEvenIfNoOutputs() - } - project.addToBuildOnServer(verifyELFRegionAlignmentTaskProvider) + project.configureVerifyELFRegionAlignment(variant) variant.aarMetadata.configureMinAgpVersion() } project.buildOnServerDependsOnAssembleRelease() @@ -1353,6 +1344,18 @@ abstract class AndroidXImplPlugin @Inject constructor() : Plugin { } } + private fun Project.configureVerifyELFRegionAlignment(variant: LibraryVariant) { + val verifyELFRegionAlignmentTaskProvider = + project.tasks.register( + variant.name + "VerifyELFRegionAlignment", + VerifyELFRegionAlignmentTask::class.java, + ) { task -> + task.mergedNativeLibs.set(variant.artifacts.get(SingleArtifact.MERGED_NATIVE_LIBS)) + task.cacheEvenIfNoOutputs() + } + project.addToBuildOnServer(verifyELFRegionAlignmentTaskProvider) + } + /** * Tells whether this build contains the usual set of all projects (`./gradlew projects`) * Sometimes developers request to include fewer projects because this may run more quickly From 4bc48b3b06fa4b3ad773b3a2061f0172e57ef0f6 Mon Sep 17 00:00:00 2001 From: Isaac Madgwick Date: Tue, 3 Feb 2026 09:54:24 +0000 Subject: [PATCH 14/21] Translate AssetLoaderAjaxActivity to kotlin Test: ./gradlew :webkit:integration-tests:testapp:connectedAndroidTest passed Bug: 460044550 Change-Id: I4e16ffa6ebcc2d5ca6470fbd20bcbb6300df1d81 --- .../webkit/AssetLoaderAjaxActivity.java | 160 ------------------ .../webkit/AssetLoaderAjaxActivity.kt | 135 +++++++++++++++ 2 files changed, 135 insertions(+), 160 deletions(-) delete mode 100644 webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/AssetLoaderAjaxActivity.java create mode 100644 webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/AssetLoaderAjaxActivity.kt diff --git a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/AssetLoaderAjaxActivity.java b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/AssetLoaderAjaxActivity.java deleted file mode 100644 index 0b688e7db9e89..0000000000000 --- a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/AssetLoaderAjaxActivity.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * 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. - */ - -package com.example.androidx.webkit; - -import android.annotation.SuppressLint; -import android.net.Uri; -import android.os.Bundle; -import android.webkit.WebResourceRequest; -import android.webkit.WebResourceResponse; -import android.webkit.WebSettings; -import android.webkit.WebView; -import android.webkit.WebViewClient; -import android.widget.Button; - -import androidx.annotation.VisibleForTesting; -import androidx.appcompat.app.AppCompatActivity; -import androidx.test.espresso.idling.net.UriIdlingResource; -import androidx.webkit.WebViewAssetLoader; -import androidx.webkit.WebViewAssetLoader.AssetsPathHandler; -import androidx.webkit.WebViewAssetLoader.ResourcesPathHandler; - -import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; - -/** - * An {@link Activity} to show a more useful use case: performing ajax calls to load files from - * local app assets and resources in a safer way using WebViewAssetLoader. - */ -public class AssetLoaderAjaxActivity extends AppCompatActivity { - private static final int MAX_IDLE_TIME_MS = 5000; - - private static class MyWebViewClient extends WebViewClient { - private final WebViewAssetLoader mAssetLoader; - private final UriIdlingResource mUriIdlingResource; - - MyWebViewClient(@NonNull WebViewAssetLoader assetLoader, - @NonNull UriIdlingResource uriIdlingResource) { - mAssetLoader = assetLoader; - mUriIdlingResource = uriIdlingResource; - } - - /** @noinspection RedundantSuppression*/ - @Override - @SuppressWarnings("deprecation") // use the old one for compatibility with all API levels. - public boolean shouldOverrideUrlLoading(WebView view, String url) { - return false; - } - - @Override - public WebResourceResponse shouldInterceptRequest(WebView view, - WebResourceRequest request) { - Uri url = request.getUrl(); - mUriIdlingResource.beginLoad(url.toString()); - WebResourceResponse response = mAssetLoader.shouldInterceptRequest(url); - mUriIdlingResource.endLoad(url.toString()); - return response; - } - - /** @noinspection RedundantSuppression*/ - @Override - @SuppressWarnings("deprecation") // use the old one for compatibility with all API levels. - public WebResourceResponse shouldInterceptRequest(WebView view, String url) { - mUriIdlingResource.beginLoad(url); - WebResourceResponse response = mAssetLoader.shouldInterceptRequest(Uri.parse(url)); - mUriIdlingResource.endLoad(url); - return response; - } - } - - private WebView mWebView; - - // IdlingResource that indicates that WebView has finished loading all WebResourceRequests - // by waiting until there are no requests made for 5000ms. - private final @NonNull UriIdlingResource mUriIdlingResource = - new UriIdlingResource("AssetLoaderWebViewUriIdlingResource", MAX_IDLE_TIME_MS); - - @SuppressLint("SetJavaScriptEnabled") - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.activity_asset_loader); - setTitle(R.string.asset_loader_ajax_activity_title); - WebkitHelpers.enableEdgeToEdge(this); - WebkitHelpers.appendWebViewVersionToTitle(this); - - // The "https://example.com" domain with the virtual path "/androidx_webkit/example/ - // is used to host resources/assets is used for demonstration purpose only. - // The developer should ALWAYS use a domain which they are in control of or use - // the default androidplatform.net reserved by Google for this purpose. - // use "example.com" instead of the default domain - // Host app resources ... under https://example.com/androidx_webkit/example/res/... - // Host app assets under https://example.com/androidx_webkit/example/assets/... - WebViewAssetLoader assetLoader = new WebViewAssetLoader.Builder() - .setDomain("example.com") // use "example.com" instead of the default domain - // Host app resources ... under https://example.com/androidx_webkit/example/res/... - .addPathHandler("/androidx_webkit/example/res/", new ResourcesPathHandler(this)) - // Host app assets under https://example.com/androidx_webkit/example/assets/... - .addPathHandler("/androidx_webkit/example/assets/", new AssetsPathHandler(this)) - .build(); - - mWebView = findViewById(R.id.webview_asset_loader_webview); - mWebView.setWebViewClient(new MyWebViewClient(assetLoader, mUriIdlingResource)); - - WebSettings webViewSettings = mWebView.getSettings(); - webViewSettings.setJavaScriptEnabled(true); - setDeprecatedAllowFileAccess(webViewSettings); - // Keeping these off is less critical but still a good idea, especially - // if your app is not using file:// or content:// URLs. - webViewSettings.setAllowFileAccess(false); - webViewSettings.setAllowContentAccess(false); - - Button loadButton = findViewById(R.id.button_load_ajax_html); - loadButton.setOnClickListener(v -> loadUrl()); - } - - /** @noinspection RedundantSuppression*/ - @SuppressWarnings("deprecation") /* b/180503860 */ - private static void setDeprecatedAllowFileAccess(WebSettings webViewSettings) { - // Setting this off for security. Off by default for SDK versions >= 16. - webViewSettings.setAllowFileAccessFromFileURLs(false); - webViewSettings.setAllowUniversalAccessFromFileURLs(false); - } - - /** - * Load the url https://example.com/androidx_webkit/example/assets/www/ajax_requests.html. - */ - public void loadUrl() { - String mainPageUrl = new Uri.Builder() - .scheme("https") - .authority("example.com") - .appendPath("androidx_webkit").appendPath("example").appendPath("assets") - .appendPath("www").appendPath("ajax_requests.html") - .build().toString(); - mWebView.loadUrl(mainPageUrl); - } - - /** - * Create and return {@link UriIdlingResource} which indicates if WebView has finished loading - * all requested URIs. - */ - @VisibleForTesting - public @NonNull UriIdlingResource getUriIdlingResource() { - return mUriIdlingResource; - } -} diff --git a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/AssetLoaderAjaxActivity.kt b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/AssetLoaderAjaxActivity.kt new file mode 100644 index 0000000000000..40ad8f1f3db93 --- /dev/null +++ b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/AssetLoaderAjaxActivity.kt @@ -0,0 +1,135 @@ +/* + * Copyright 2026 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 com.example.androidx.webkit + +import android.net.Uri +import android.os.Bundle +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import android.webkit.WebSettings +import android.webkit.WebView +import android.webkit.WebViewClient +import android.widget.Button +import androidx.appcompat.app.AppCompatActivity +import androidx.test.espresso.idling.net.UriIdlingResource +import androidx.webkit.WebViewAssetLoader + +/** + * An {@link Activity} to show a more useful use case: performing ajax calls to load files from + * local app assets and resources in a safer way using WebViewAssetLoader. + */ +class AssetLoaderAjaxActivity : AppCompatActivity() { + + val MAX_IDLE_TIME_MS = 5000L + + private class MyWebViewClient( + private val assetLoader: WebViewAssetLoader, + val uriIdlingResource: UriIdlingResource, + ) : WebViewClient() { + + @Deprecated( + "Intentional use of deprecation" + ) // use the old one for compatibility with all API levels + override fun shouldOverrideUrlLoading(view: WebView, url: String) = false + + @Deprecated( + "Intentional use of deprecation" + ) // use the old one for compatibility with all API levels + override fun shouldInterceptRequest(view: WebView, url: String): WebResourceResponse? { + uriIdlingResource.beginLoad(url) + val response = assetLoader.shouldInterceptRequest(Uri.parse(url)) + uriIdlingResource.endLoad(url) + return response + } + + override fun shouldInterceptRequest( + view: WebView, + request: WebResourceRequest, + ): WebResourceResponse? { + val url = request.url + uriIdlingResource.beginLoad(url.toString()) + val response = assetLoader.shouldInterceptRequest(url) + uriIdlingResource.endLoad(url.toString()) + return response + } + } + + // IdlingResource that indicates that WebView has finished loading all WebResourceRequests + // by waiting until there are no requests made for 5000ms. + val uriIdlingResource = + UriIdlingResource("AssetLoaderWebViewUriIdlingResource", MAX_IDLE_TIME_MS) + private lateinit var webView: WebView + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.activity_asset_loader) + setTitle(R.string.asset_loader_ajax_activity_title) + WebkitHelpers.enableEdgeToEdge(this) + WebkitHelpers.appendWebViewVersionToTitle(this) + + // The "https://example.com" domain with the virtual path "/androidx_webkit/example/ + // is used to host resources/assets is used for demonstration purpose only. + // The developer should ALWAYS use a domain which they are in control of or use + // the default androidplatform.net reserved by Google for this purpose. + // use "example.com" instead of the default domain + // Host app resources ... under https://example.com/androidx_webkit/example/res/... + // Host app assets under https://example.com/androidx_webkit/example/assets/... + val assetLoader = + WebViewAssetLoader.Builder() + .setDomain("example.com") // use "example.com" instead of the default domain + // Host app resources ... under https://example.com/androidx_webkit/example/res/... + .addPathHandler( + "/androidx_webkit/example/res/", + WebViewAssetLoader.ResourcesPathHandler(this), + ) + // Host app assets under https://example.com/androidx_webkit/example/assets/... + .addPathHandler( + "/androidx_webkit/example/assets/", + WebViewAssetLoader.AssetsPathHandler(this), + ) + .build() + + webView = findViewById(R.id.webview_asset_loader_webview) + webView.webViewClient = MyWebViewClient(assetLoader, uriIdlingResource) + + with(webView.settings) { + javaScriptEnabled = true + setDeprecatedAllowFileAccess(this) + // Keeping these off is less critical but still a good idea, especially + // if your app is not using file:// or content:// URLs. + allowFileAccess = true + allowContentAccess = true + } + + val loadButton: Button = findViewById(R.id.button_load_ajax_html) + loadButton.setOnClickListener { loadUrl() } + } + + /** @noinspection RedundantSuppression */ + @Suppress("DEPRECATION") /* b/180503860 */ + private fun setDeprecatedAllowFileAccess(webViewSettings: WebSettings) { + // Setting this off for security. Off by default for SDK versions >= 16. + webViewSettings.allowFileAccessFromFileURLs = false + webViewSettings.allowUniversalAccessFromFileURLs = false + } + + /** Load the url https://example.com/androidx_webkit/example/assets/www/ajax_requests.html. */ + fun loadUrl() { + webView.loadUrl("https://example.com/androidx_webkit/example/assets/www/ajax_requests.html") + } +} From 2c8d472e2e71b085c8ca9bb8a62a503ab54c659e Mon Sep 17 00:00:00 2001 From: stevebower Date: Fri, 6 Feb 2026 14:22:30 +0000 Subject: [PATCH 15/21] Update documentation for WearComposeFoundationFlags and isTransformingLazyColumnClickableThresholdEnabled flag Bug: 482012112 Bug: 480910891 Test: N/A Relnote: N/A Change-Id: I275d220ffdfacb3c85dd8e243bef8f7e6210faef --- .../compose/foundation/WearComposeFoundationFlags.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/WearComposeFoundationFlags.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/WearComposeFoundationFlags.kt index 9d026ba3ae87e..afa11599260ae 100644 --- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/WearComposeFoundationFlags.kt +++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/WearComposeFoundationFlags.kt @@ -44,13 +44,19 @@ package androidx.wear.compose.foundation * paths being completely removed from the artifact, which can often have nontrivial positive * performance impact. * - * -assumevalues class androidx.wear.compose.runtime.WearComposeFoundationFlags { + * -assumevalues class androidx.wear.compose.foundation.WearComposeFoundationFlags { * public static boolean SomeFeatureEnabled return false * } */ @ExperimentalWearFoundationApi public object WearComposeFoundationFlags { - /** Whether to use TransformingLazyColumn clickable threshold. */ + /** + * Whether to use the new clickability threshold in + * [androidx.wear.compose.foundation.lazy.TransformingLazyColumn]. When true, the clickability + * threshold ignores clicks in the top or bottom 20dp of the layout, to avoid accidental clicks + * on small items that are partially shown due to fading/scaling in the TransformingLazyColumn. + * If false, all clicks will be recognized instead + */ @field:Suppress("MutableBareField") @JvmField public var isTransformingLazyColumnClickableThresholdEnabled: Boolean = true From c9ebf6fca223b02676296da50a49da10fb221ba1 Mon Sep 17 00:00:00 2001 From: Derek Xu Date: Fri, 6 Feb 2026 07:14:49 -0800 Subject: [PATCH 16/21] Revert "Introduce internal `Monitor` class" This reverts commit d09eba2737a4a7c640f6f2baf82134d168806d4a. Reason for revert: The only usages of this class were reverted and it is unknown when they will be relanded (see b/418800424 for more details). Fixes: 477633861 Change-Id: I8cc28a69d5e313791f179a557391745ed891f017 --- compose/runtime/runtime/build.gradle | 6 -- .../runtime/platform/Synchronization.apple.kt | 19 ----- .../runtime/platform/Synchronization.kt | 41 ---------- .../compose/runtime/Synchronization.jvm.kt | 35 --------- .../runtime/platform/Synchronization.linux.kt | 19 ----- .../platform/Synchronization.mingwX64.kt | 69 ----------------- .../platform/Synchronization.native.kt | 62 ---------------- .../androidx/compose/runtime/MonitorTests.kt | 53 ------------- .../runtime/platform/Synchronization.unix.kt | 74 ------------------- .../runtime/platform/Synchronization.web.kt | 10 --- 10 files changed, 388 deletions(-) delete mode 100644 compose/runtime/runtime/src/appleMain/kotlin/androidx/compose/runtime/platform/Synchronization.apple.kt delete mode 100644 compose/runtime/runtime/src/jvmAndAndroidMain/kotlin/androidx/compose/runtime/Synchronization.jvm.kt delete mode 100644 compose/runtime/runtime/src/linuxMain/kotlin/androidx/compose/runtime/platform/Synchronization.linux.kt delete mode 100644 compose/runtime/runtime/src/mingwX64Main/kotlin/androidx/compose/runtime/platform/Synchronization.mingwX64.kt delete mode 100644 compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/MonitorTests.kt delete mode 100644 compose/runtime/runtime/src/unixMain/kotlin/androidx/compose/runtime/platform/Synchronization.unix.kt diff --git a/compose/runtime/runtime/build.gradle b/compose/runtime/runtime/build.gradle index 9cfe79b98d871..335d81c440ab0 100644 --- a/compose/runtime/runtime/build.gradle +++ b/compose/runtime/runtime/build.gradle @@ -115,12 +115,6 @@ androidXMultiplatform { wasmJsMain {}.dependencies { implementation(libs.kotlinXw3c) } - - unixMain {}.dependsOn(nativeMain) - - appleMain {}.dependsOn(unixMain) - - linuxMain.dependsOn(unixMain) } } diff --git a/compose/runtime/runtime/src/appleMain/kotlin/androidx/compose/runtime/platform/Synchronization.apple.kt b/compose/runtime/runtime/src/appleMain/kotlin/androidx/compose/runtime/platform/Synchronization.apple.kt deleted file mode 100644 index 7f7ad03813558..0000000000000 --- a/compose/runtime/runtime/src/appleMain/kotlin/androidx/compose/runtime/platform/Synchronization.apple.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2026 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.platform - -internal actual val PTHREAD_MUTEX_RECURSIVE: Int = platform.posix.PTHREAD_MUTEX_RECURSIVE diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/platform/Synchronization.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/platform/Synchronization.kt index 90f807d072ef2..a8ef89988d187 100644 --- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/platform/Synchronization.kt +++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/platform/Synchronization.kt @@ -28,44 +28,3 @@ internal expect inline fun makeSynchronizedObject(ref: Any? = null): Synchronize @PublishedApi @Suppress("LESS_VISIBLE_TYPE_ACCESS_IN_INLINE_WARNING") // b/446705238 internal expect inline fun synchronized(lock: SynchronizedObject, block: () -> R): R - -/** - * An implementation of the "monitor" synchronization construct. - * - * This class should only be used when [wait] and [notifyAll] are needed, because [synchronized] - * blocks that synchronize on [SynchronizedObject]s are more performant than ones that synchronize - * on [Monitor]s. - * - * Every method of this class is a no-op on Kotlin/JS and Kotlin/Wasm because they are - * single-threaded. - */ -internal expect class Monitor { - /** - * Causes the current thread to wait until another thread invokes the [notifyAll] method on this - * object. - * - * This method should only be called by a thread that is the owner of this monitor. A thread - * becomes the owner of a monitor by executing the body of a [synchronized] statement that - * synchronizes on the monitor. Calling this method relases the monitor. The monitor will be - * re-acquired before this thread resumes execution. - */ - fun wait() - - /** - * Wakes up all threads that are waiting on this monitor. - * - * This method should only be called by a thread that is the owner of this monitor. A thread - * becomes the owner of a monitor by executing the body of a [synchronized] statement that - * synchronizes on the monitor. - */ - fun notifyAll() -} - -/** - * Returns [ref] as a [Monitor] on platforms where [Any] is a valid [Monitor], or a new [Monitor] - * instance if [ref] is null or [ref] cannot be cast to [Monitor] on the current platform. - */ -internal expect inline fun makeMonitor(ref: Any? = null): Monitor - -/** Sequentially acquires [monitor], executes [block], and releases [monitor]. */ -internal expect inline fun synchronized(monitor: Monitor, block: () -> R): R diff --git a/compose/runtime/runtime/src/jvmAndAndroidMain/kotlin/androidx/compose/runtime/Synchronization.jvm.kt b/compose/runtime/runtime/src/jvmAndAndroidMain/kotlin/androidx/compose/runtime/Synchronization.jvm.kt deleted file mode 100644 index ad8362b40a66f..0000000000000 --- a/compose/runtime/runtime/src/jvmAndAndroidMain/kotlin/androidx/compose/runtime/Synchronization.jvm.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2026 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("PLATFORM_CLASS_MAPPED_TO_KOTLIN") // b/477633861 - -package androidx.compose.runtime.platform - -import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract - -internal actual typealias Monitor = Object - -@Suppress("NOTHING_TO_INLINE") -internal actual inline fun makeMonitor(ref: Any?) = if (ref == null) Monitor() else ref as Monitor - -@Suppress("BanInlineOptIn") -@OptIn(ExperimentalContracts::class) -internal actual inline fun synchronized(monitor: Monitor, block: () -> R): R { - contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } - return kotlin.synchronized(monitor, block) -} diff --git a/compose/runtime/runtime/src/linuxMain/kotlin/androidx/compose/runtime/platform/Synchronization.linux.kt b/compose/runtime/runtime/src/linuxMain/kotlin/androidx/compose/runtime/platform/Synchronization.linux.kt deleted file mode 100644 index 6cf84f07b3ba9..0000000000000 --- a/compose/runtime/runtime/src/linuxMain/kotlin/androidx/compose/runtime/platform/Synchronization.linux.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2026 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.platform - -internal actual val PTHREAD_MUTEX_RECURSIVE: Int = platform.posix.PTHREAD_MUTEX_RECURSIVE.toInt() diff --git a/compose/runtime/runtime/src/mingwX64Main/kotlin/androidx/compose/runtime/platform/Synchronization.mingwX64.kt b/compose/runtime/runtime/src/mingwX64Main/kotlin/androidx/compose/runtime/platform/Synchronization.mingwX64.kt deleted file mode 100644 index a20ebef62f178..0000000000000 --- a/compose/runtime/runtime/src/mingwX64Main/kotlin/androidx/compose/runtime/platform/Synchronization.mingwX64.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2026 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.platform - -import kotlinx.cinterop.Arena -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.LongVarOf -import kotlinx.cinterop.UIntVarOf -import kotlinx.cinterop.alloc -import kotlinx.cinterop.ptr -import platform.posix.PTHREAD_MUTEX_RECURSIVE -import platform.posix.pthread_cond_broadcast -import platform.posix.pthread_cond_destroy -import platform.posix.pthread_cond_init -import platform.posix.pthread_cond_t -import platform.posix.pthread_cond_wait -import platform.posix.pthread_mutex_destroy -import platform.posix.pthread_mutex_init -import platform.posix.pthread_mutex_lock -import platform.posix.pthread_mutex_t -import platform.posix.pthread_mutex_unlock -import platform.posix.pthread_mutexattr_destroy -import platform.posix.pthread_mutexattr_init -import platform.posix.pthread_mutexattr_settype -import platform.posix.pthread_mutexattr_t - -@OptIn(ExperimentalForeignApi::class) -internal actual class NativeMonitor { - private val arena: Arena = Arena() - private val cond: LongVarOf = arena.alloc() - private val mutex: LongVarOf = arena.alloc() - private val attr: UIntVarOf = arena.alloc() - - init { - require(pthread_cond_init(cond.ptr, null) == 0) - require(pthread_mutexattr_init(attr.ptr) == 0) - require(pthread_mutexattr_settype(attr.ptr, PTHREAD_MUTEX_RECURSIVE) == 0) - require(pthread_mutex_init(mutex.ptr, attr.ptr) == 0) - } - - actual fun enter() = require(pthread_mutex_lock(mutex.ptr) == 0) - - actual fun exit() = require(pthread_mutex_unlock(mutex.ptr) == 0) - - actual fun wait() = require(pthread_cond_wait(cond.ptr, mutex.ptr) == 0) - - actual fun notifyAll() = require(pthread_cond_broadcast(cond.ptr) == 0) - - actual fun dispose() { - pthread_cond_destroy(cond.ptr) - pthread_mutex_destroy(mutex.ptr) - pthread_mutexattr_destroy(attr.ptr) - arena.clear() - } -} diff --git a/compose/runtime/runtime/src/nativeMain/kotlin/androidx/compose/runtime/platform/Synchronization.native.kt b/compose/runtime/runtime/src/nativeMain/kotlin/androidx/compose/runtime/platform/Synchronization.native.kt index 428f9e5299b22..b8d34c5cd9fe5 100644 --- a/compose/runtime/runtime/src/nativeMain/kotlin/androidx/compose/runtime/platform/Synchronization.native.kt +++ b/compose/runtime/runtime/src/nativeMain/kotlin/androidx/compose/runtime/platform/Synchronization.native.kt @@ -19,8 +19,6 @@ package androidx.compose.runtime.platform import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract -import kotlin.experimental.ExperimentalNativeApi -import kotlin.native.ref.createCleaner @PublishedApi @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") @@ -36,63 +34,3 @@ internal actual inline fun synchronized(lock: SynchronizedObject, block: () contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return kotlinx.atomicfu.locks.synchronized(lock, block) } - -/** - * This class should never be used outside of this file. It is just a helper class that was added to - * minimize code duplication between the Unix and mingwX64 implementations of [Monitor]. - */ -internal expect class NativeMonitor() { - fun enter() - - fun exit() - - fun wait() - - fun notifyAll() - - fun dispose() -} - -private class MonitorWrapper { - val monitor: NativeMonitor = NativeMonitor() - - @OptIn(ExperimentalNativeApi::class) - val cleaner = createCleaner(monitor, NativeMonitor::dispose) -} - -internal actual class Monitor { - private val monitorWrapper: MonitorWrapper by lazy { MonitorWrapper() } - private val monitor: NativeMonitor - get() = monitorWrapper.monitor - - @PublishedApi - internal fun lock() { - monitor.enter() - } - - @PublishedApi - internal fun unlock() { - monitor.exit() - } - - actual fun wait() { - monitor.wait() - } - - actual fun notifyAll() { - monitor.notifyAll() - } -} - -@Suppress("NOTHING_TO_INLINE") internal actual inline fun makeMonitor(ref: Any?) = Monitor() - -internal actual inline fun synchronized(monitor: Monitor, block: () -> R): R { - monitor.run { - lock() - return try { - block() - } finally { - unlock() - } - } -} diff --git a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/MonitorTests.kt b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/MonitorTests.kt deleted file mode 100644 index cf9c3b385ce59..0000000000000 --- a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/MonitorTests.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2025 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.internal.AtomicInt -import androidx.compose.runtime.platform.makeMonitor -import androidx.compose.runtime.platform.synchronized -import kotlin.test.Test -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlinx.test.IgnoreJsTarget -import kotlinx.test.IgnoreWasmTarget - -class MonitorTests { - @Test - @IgnoreJsTarget - @IgnoreWasmTarget - fun testWaitAndNotifyAll() = runTest { - val counter = AtomicInt(0) - val monitor = makeMonitor() - - withContext(Dispatchers.Default) { - val numJobs = 3 - repeat(numJobs) { - launch { - if (counter.add(1) == numJobs) { - synchronized(monitor) { monitor.notifyAll() } - } else { - synchronized(monitor) { monitor.wait() } - // If `wait` does not block as expected, `counter` will not be able to - // reach `numJobs` and the test will time out. - counter.add(-1) - } - } - } - } - } -} diff --git a/compose/runtime/runtime/src/unixMain/kotlin/androidx/compose/runtime/platform/Synchronization.unix.kt b/compose/runtime/runtime/src/unixMain/kotlin/androidx/compose/runtime/platform/Synchronization.unix.kt deleted file mode 100644 index 96fbc372e3f15..0000000000000 --- a/compose/runtime/runtime/src/unixMain/kotlin/androidx/compose/runtime/platform/Synchronization.unix.kt +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2026 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.platform - -import kotlinx.cinterop.Arena -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.alloc -import kotlinx.cinterop.ptr -import platform.posix.pthread_cond_broadcast -import platform.posix.pthread_cond_destroy -import platform.posix.pthread_cond_init -import platform.posix.pthread_cond_t -import platform.posix.pthread_cond_wait -import platform.posix.pthread_mutex_destroy -import platform.posix.pthread_mutex_init -import platform.posix.pthread_mutex_lock -import platform.posix.pthread_mutex_t -import platform.posix.pthread_mutex_unlock -import platform.posix.pthread_mutexattr_destroy -import platform.posix.pthread_mutexattr_init -import platform.posix.pthread_mutexattr_settype -import platform.posix.pthread_mutexattr_t - -/** - * Wrapper for `platform.posix.PTHREAD_MUTEX_RECURSIVE`, which is represented as `kotlin.Int` on - * Apple platforms and `kotlin.UInt` on linuxX64. - * - * See: [KT-41509](https://youtrack.jetbrains.com/issue/KT-41509) - */ -internal expect val PTHREAD_MUTEX_RECURSIVE: Int - -@OptIn(ExperimentalForeignApi::class) -internal actual class NativeMonitor { - private val arena: Arena = Arena() - private val cond: pthread_cond_t = arena.alloc() - private val mutex: pthread_mutex_t = arena.alloc() - private val attr: pthread_mutexattr_t = arena.alloc() - - init { - require(pthread_cond_init(cond.ptr, null) == 0) - require(pthread_mutexattr_init(attr.ptr) == 0) - require(pthread_mutexattr_settype(attr.ptr, PTHREAD_MUTEX_RECURSIVE) == 0) - require(pthread_mutex_init(mutex.ptr, attr.ptr) == 0) - } - - actual fun enter() = require(pthread_mutex_lock(mutex.ptr) == 0) - - actual fun exit() = require(pthread_mutex_unlock(mutex.ptr) == 0) - - actual fun wait() = require(pthread_cond_wait(cond.ptr, mutex.ptr) == 0) - - actual fun notifyAll() = require(pthread_cond_broadcast(cond.ptr) == 0) - - actual fun dispose() { - pthread_cond_destroy(cond.ptr) - pthread_mutex_destroy(mutex.ptr) - pthread_mutexattr_destroy(attr.ptr) - arena.clear() - } -} diff --git a/compose/runtime/runtime/src/webMain/kotlin/androidx/compose/runtime/platform/Synchronization.web.kt b/compose/runtime/runtime/src/webMain/kotlin/androidx/compose/runtime/platform/Synchronization.web.kt index 39195d9d1db5b..ddc557f775e9e 100644 --- a/compose/runtime/runtime/src/webMain/kotlin/androidx/compose/runtime/platform/Synchronization.web.kt +++ b/compose/runtime/runtime/src/webMain/kotlin/androidx/compose/runtime/platform/Synchronization.web.kt @@ -23,13 +23,3 @@ internal actual inline fun makeSynchronizedObject(ref: Any?) = ref ?: Synchroniz @PublishedApi internal actual inline fun synchronized(lock: SynchronizedObject, block: () -> R): R = block() - -internal actual class Monitor { - actual fun wait() {} - - actual fun notifyAll() {} -} - -@Suppress("NOTHING_TO_INLINE") internal actual inline fun makeMonitor(ref: Any?) = Monitor() - -internal actual inline fun synchronized(monitor: Monitor, block: () -> R): R = block() From d048ae9c0e2f5abd37b362c4ba09eb975a8a9e16 Mon Sep 17 00:00:00 2001 From: rahulkanojia Date: Fri, 6 Feb 2026 20:51:37 +0530 Subject: [PATCH 17/21] fix: Race condition in PdfDocumentViewModelTest using UnconfinedTestDispatcher The test test_pdfDocumentViewModel_noSearchOnSameQuery was failing consistently at the initial state assertion. This was caused by the default StandardTestDispatcher scheduling the collection coroutine lazily, allowing the assertion to execute before the initial StateFlow value was collected. Bug: b/482329265 Test: ./gradlew :pdf:pdf-viewer-fragment:connectedAndroidTest Change-Id: I0ed566005252e8d2762da3263ed157a836404718 --- .../PdfDocumentViewSearchScenarioTest.kt | 40 +++++++++++++------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/pdf/pdf-viewer-fragment/src/androidTest/kotlin/androidx/pdf/viewer/fragment/PdfDocumentViewSearchScenarioTest.kt b/pdf/pdf-viewer-fragment/src/androidTest/kotlin/androidx/pdf/viewer/fragment/PdfDocumentViewSearchScenarioTest.kt index 064b2316ef976..eab0d85a4627b 100644 --- a/pdf/pdf-viewer-fragment/src/androidTest/kotlin/androidx/pdf/viewer/fragment/PdfDocumentViewSearchScenarioTest.kt +++ b/pdf/pdf-viewer-fragment/src/androidTest/kotlin/androidx/pdf/viewer/fragment/PdfDocumentViewSearchScenarioTest.kt @@ -31,16 +31,17 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import androidx.test.platform.app.InstrumentationRegistry import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -342,37 +343,50 @@ class PdfDocumentViewSearchScenarioTest { assertEquals(totalSearchMatches, updatedHighlightData.highlightBounds.size) } - @Ignore("Need more investigation on why it's failing on post submits. b/386727907") @Test fun test_pdfDocumentViewModel_noSearchOnSameQuery() = runTest { - var counter = 0 - val collectorJob = launch { pdfDocumentViewModel.searchViewUiState.collect { counter++ } } - val fakeResults = createFakeSearchResults(0, 1, 2, 2, 5, 5, 10, 10, 10, 10) setupViewModel(fakeResults) + + val searchUiStates = mutableListOf() + val collectedJob = + backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { + pdfDocumentViewModel.searchViewUiState.collect { searchUiStates.add(it) } + } + + // Assert initially closed state is collected + assertEquals(1, searchUiStates.size) + assertTrue(searchUiStates.first() is SearchViewUiState.Closed) + pdfDocumentViewModel.loadDocument(uri = documentUri, password = null) - // wait for document to load - advanceUntilIdle() + // Wait for document to load + pdfDocumentViewModel.fragmentUiScreenState.first { it is PdfFragmentUiState.DocumentLoaded } + // turn on search pdfDocumentViewModel.updateSearchState(true) // assert search view Init state is collected - assertEquals(1, counter) + assertTrue(searchUiStates[1] is SearchViewUiState.Init) assertTrue(pdfDocumentViewModel.searchViewUiState.value is SearchViewUiState.Init) // start search pdfDocumentViewModel.searchDocument(query = SEARCH_QUERY, visiblePageRange = IntRange(0, 1)) - // wait for search operation to complete - advanceUntilIdle() + // wait for result + pdfDocumentViewModel.searchViewUiState.first { it is SearchViewUiState.Active } + // assert new state emitted after search completion - assertEquals(2, counter) + assertEquals(3, searchUiStates.size) + val activeState = searchUiStates[2] as SearchViewUiState.Active + assertEquals(SEARCH_QUERY, activeState.query) + assertEquals(1, activeState.currentMatch) + assertEquals(10, activeState.totalMatches) // search with the same query again pdfDocumentViewModel.searchDocument(query = SEARCH_QUERY, visiblePageRange = IntRange(0, 1)) advanceUntilIdle() // Assert no new state is emitted - assertEquals(2, counter) + assertEquals(3, searchUiStates.size) - collectorJob.cancel() + collectedJob.cancel() } @Test From ab0b9ea94f83866f934ec37ee2f4586806449036 Mon Sep 17 00:00:00 2001 From: aplachykau Date: Mon, 26 Jan 2026 16:20:37 +0100 Subject: [PATCH 18/21] Add isAtTopState overload for AppBar scroll behaviors Added overloads for `TopAppBarDefaults.enterAlwaysScrollBehavior` and `TopAppBarDefaults.pinnedScrollBehavior` that accept an `isAtTopState` lambda. This allows for custom 'at top' detection, which is necessary for complex layouts like a reversed `LazyVerticalGrid`. Updated tests and samples to demonstrate the new API usage. Bug: 405129274 Test: AppBarTest.kt Relnote: "Added overloads to `TopAppBarDefaults.enterAlwaysScrollBehavior` and `TopAppBarDefaults.pinnedScrollBehavior` that accept an `isAtTopState` parameter. This allows for custom `at top state` detection when using the scroll behaviors." Change-Id: I785b977d3981b20a2158302f9b4a10ab52265722 --- compose/material3/material3/api/current.txt | 4 + .../material3/api/restricted_current.txt | 4 + .../catalog/library/model/Examples.kt | 27 ++ .../material3/samples/AppBarSamples.kt | 191 +++++++++ .../androidx/compose/material3/AppBarTest.kt | 388 ++++++++++++++++++ .../androidx/compose/material3/AppBar.kt | 112 +++-- 6 files changed, 698 insertions(+), 28 deletions(-) diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt index 80ba259af723b..f8bf0ad1c541c 100644 --- a/compose/material3/material3/api/current.txt +++ b/compose/material3/material3/api/current.txt @@ -4605,6 +4605,8 @@ package androidx.compose.material3 { method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior enterAlwaysScrollBehavior(androidx.compose.material3.TopAppBarState?, kotlin.jvm.functions.Function0?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.animation.core.DecayAnimationSpec?, androidx.compose.runtime.Composer?, int, int); method @KotlinOnly @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior enterAlwaysScrollBehavior(optional androidx.compose.material3.TopAppBarState state, optional kotlin.jvm.functions.Function0 canScroll, optional androidx.compose.animation.core.AnimationSpec? snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec? flingAnimationSpec, optional boolean reverseLayout); method @BytecodeOnly @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior enterAlwaysScrollBehavior(androidx.compose.material3.TopAppBarState?, kotlin.jvm.functions.Function0?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.animation.core.DecayAnimationSpec?, boolean, androidx.compose.runtime.Composer?, int, int); + method @KotlinOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior enterAlwaysScrollBehavior(optional androidx.compose.material3.TopAppBarState state, optional kotlin.jvm.functions.Function0 canScroll, optional androidx.compose.animation.core.AnimationSpec? snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec? flingAnimationSpec, optional kotlin.jvm.functions.Function0 isAtTopState); + method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior enterAlwaysScrollBehavior(androidx.compose.material3.TopAppBarState?, kotlin.jvm.functions.Function0?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.animation.core.DecayAnimationSpec?, kotlin.jvm.functions.Function0?, androidx.compose.runtime.Composer?, int, int); method @KotlinOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior exitUntilCollapsedScrollBehavior(optional androidx.compose.material3.TopAppBarState state, optional kotlin.jvm.functions.Function0 canScroll, optional androidx.compose.animation.core.AnimationSpec? snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec? flingAnimationSpec); method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior exitUntilCollapsedScrollBehavior(androidx.compose.material3.TopAppBarState?, kotlin.jvm.functions.Function0?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.animation.core.DecayAnimationSpec?, androidx.compose.runtime.Composer?, int, int); method @InaccessibleFromKotlin public androidx.compose.foundation.layout.PaddingValues getContentPadding(); @@ -4632,6 +4634,8 @@ package androidx.compose.material3 { method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior pinnedScrollBehavior(androidx.compose.foundation.ScrollState, boolean, androidx.compose.material3.TopAppBarState?, kotlin.jvm.functions.Function0?, androidx.compose.runtime.Composer?, int, int); method @KotlinOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior pinnedScrollBehavior(optional androidx.compose.material3.TopAppBarState state, optional kotlin.jvm.functions.Function0 canScroll); method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior pinnedScrollBehavior(androidx.compose.material3.TopAppBarState?, kotlin.jvm.functions.Function0?, androidx.compose.runtime.Composer?, int, int); + method @KotlinOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior pinnedScrollBehavior(optional androidx.compose.material3.TopAppBarState state, optional kotlin.jvm.functions.Function0 canScroll, optional kotlin.jvm.functions.Function0 isAtTopState); + method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior pinnedScrollBehavior(androidx.compose.material3.TopAppBarState?, kotlin.jvm.functions.Function0?, kotlin.jvm.functions.Function0?, androidx.compose.runtime.Composer?, int, int); method @KotlinOnly @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarColors topAppBarColors(); method @BytecodeOnly @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarColors topAppBarColors(androidx.compose.runtime.Composer?, int); method @KotlinOnly @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarColors topAppBarColors(optional androidx.compose.ui.graphics.Color containerColor, optional androidx.compose.ui.graphics.Color scrolledContainerColor, optional androidx.compose.ui.graphics.Color navigationIconContentColor, optional androidx.compose.ui.graphics.Color titleContentColor, optional androidx.compose.ui.graphics.Color actionIconContentColor, optional androidx.compose.ui.graphics.Color subtitleContentColor); diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt index 80ba259af723b..f8bf0ad1c541c 100644 --- a/compose/material3/material3/api/restricted_current.txt +++ b/compose/material3/material3/api/restricted_current.txt @@ -4605,6 +4605,8 @@ package androidx.compose.material3 { method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior enterAlwaysScrollBehavior(androidx.compose.material3.TopAppBarState?, kotlin.jvm.functions.Function0?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.animation.core.DecayAnimationSpec?, androidx.compose.runtime.Composer?, int, int); method @KotlinOnly @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior enterAlwaysScrollBehavior(optional androidx.compose.material3.TopAppBarState state, optional kotlin.jvm.functions.Function0 canScroll, optional androidx.compose.animation.core.AnimationSpec? snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec? flingAnimationSpec, optional boolean reverseLayout); method @BytecodeOnly @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior enterAlwaysScrollBehavior(androidx.compose.material3.TopAppBarState?, kotlin.jvm.functions.Function0?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.animation.core.DecayAnimationSpec?, boolean, androidx.compose.runtime.Composer?, int, int); + method @KotlinOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior enterAlwaysScrollBehavior(optional androidx.compose.material3.TopAppBarState state, optional kotlin.jvm.functions.Function0 canScroll, optional androidx.compose.animation.core.AnimationSpec? snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec? flingAnimationSpec, optional kotlin.jvm.functions.Function0 isAtTopState); + method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior enterAlwaysScrollBehavior(androidx.compose.material3.TopAppBarState?, kotlin.jvm.functions.Function0?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.animation.core.DecayAnimationSpec?, kotlin.jvm.functions.Function0?, androidx.compose.runtime.Composer?, int, int); method @KotlinOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior exitUntilCollapsedScrollBehavior(optional androidx.compose.material3.TopAppBarState state, optional kotlin.jvm.functions.Function0 canScroll, optional androidx.compose.animation.core.AnimationSpec? snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec? flingAnimationSpec); method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior exitUntilCollapsedScrollBehavior(androidx.compose.material3.TopAppBarState?, kotlin.jvm.functions.Function0?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.animation.core.DecayAnimationSpec?, androidx.compose.runtime.Composer?, int, int); method @InaccessibleFromKotlin public androidx.compose.foundation.layout.PaddingValues getContentPadding(); @@ -4632,6 +4634,8 @@ package androidx.compose.material3 { method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior pinnedScrollBehavior(androidx.compose.foundation.ScrollState, boolean, androidx.compose.material3.TopAppBarState?, kotlin.jvm.functions.Function0?, androidx.compose.runtime.Composer?, int, int); method @KotlinOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior pinnedScrollBehavior(optional androidx.compose.material3.TopAppBarState state, optional kotlin.jvm.functions.Function0 canScroll); method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior pinnedScrollBehavior(androidx.compose.material3.TopAppBarState?, kotlin.jvm.functions.Function0?, androidx.compose.runtime.Composer?, int, int); + method @KotlinOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior pinnedScrollBehavior(optional androidx.compose.material3.TopAppBarState state, optional kotlin.jvm.functions.Function0 canScroll, optional kotlin.jvm.functions.Function0 isAtTopState); + method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarScrollBehavior pinnedScrollBehavior(androidx.compose.material3.TopAppBarState?, kotlin.jvm.functions.Function0?, kotlin.jvm.functions.Function0?, androidx.compose.runtime.Composer?, int, int); method @KotlinOnly @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarColors topAppBarColors(); method @BytecodeOnly @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarColors topAppBarColors(androidx.compose.runtime.Composer?, int); method @KotlinOnly @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarColors topAppBarColors(optional androidx.compose.ui.graphics.Color containerColor, optional androidx.compose.ui.graphics.Color scrolledContainerColor, optional androidx.compose.ui.graphics.Color navigationIconContentColor, optional androidx.compose.ui.graphics.Color titleContentColor, optional androidx.compose.ui.graphics.Color actionIconContentColor, optional androidx.compose.ui.graphics.Color subtitleContentColor); diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt index 319c13efad60f..7b4d34e6362f5 100644 --- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt +++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt @@ -84,6 +84,7 @@ import androidx.compose.material3.samples.ElevatedSplitButtonSample import androidx.compose.material3.samples.ElevatedSuggestionChipSample import androidx.compose.material3.samples.ElevatedToggleButtonSample import androidx.compose.material3.samples.EnterAlwaysTopAppBar +import androidx.compose.material3.samples.EnterAlwaysTopAppBarWithReverseScrolling import androidx.compose.material3.samples.ExitAlwaysBottomAppBar import androidx.compose.material3.samples.ExitAlwaysBottomAppBarFixed import androidx.compose.material3.samples.ExitAlwaysBottomAppBarFixedVibrant @@ -183,6 +184,8 @@ import androidx.compose.material3.samples.OverflowingVerticalFloatingToolbarSamp import androidx.compose.material3.samples.PasswordTextField import androidx.compose.material3.samples.PermanentNavigationDrawerSample import androidx.compose.material3.samples.PinnedTopAppBar +import androidx.compose.material3.samples.PinnedTopAppBarWithPreScrolledLazyColumn +import androidx.compose.material3.samples.PinnedTopAppBarWithReversedLazyGrid import androidx.compose.material3.samples.PlainTooltipSample import androidx.compose.material3.samples.PlainTooltipWithCaret import androidx.compose.material3.samples.PlainTooltipWithCaretBelowAnchor @@ -1064,6 +1067,22 @@ val TopAppBarExamples = ) { PinnedTopAppBar() }, + Example( + name = "PinnedTopAppBarWithPreScrolledLazyColumn", + description = TopAppBarExampleDescription, + sourceUrl = TopAppBarExampleSourceUrl, + isExpressive = false, + ) { + PinnedTopAppBarWithPreScrolledLazyColumn() + }, + Example( + name = "PinnedTopAppBarWithReversedLazyGrid", + description = TopAppBarExampleDescription, + sourceUrl = TopAppBarExampleSourceUrl, + isExpressive = false, + ) { + PinnedTopAppBarWithReversedLazyGrid() + }, Example( name = "EnterAlwaysTopAppBar", description = TopAppBarExampleDescription, @@ -1072,6 +1091,14 @@ val TopAppBarExamples = ) { EnterAlwaysTopAppBar() }, + Example( + name = "EnterAlwaysTopAppBarWithReverseScrolling", + description = TopAppBarExampleDescription, + sourceUrl = TopAppBarExampleSourceUrl, + isExpressive = true, + ) { + EnterAlwaysTopAppBarWithReverseScrolling() + }, Example( name = "ExitUntilCollapsedMediumTopAppBar", description = TopAppBarExampleDescription, diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt index 7527455c1929e..f7ab51f35363d 100644 --- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt +++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt @@ -18,13 +18,20 @@ package androidx.compose.material3.samples import androidx.annotation.Sampled import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +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.width import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -73,6 +80,7 @@ import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.rememberTooltipState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -538,6 +546,132 @@ fun PinnedTopAppBar() { ) } +/** + * A sample for a pinned small [TopAppBar]. + * + * The top app bar here is pinned to its location and changes its container color when the content + * under it is scrolled. The content of the [LazyColumn] is pre-scrolled. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Sampled +@Composable +fun PinnedTopAppBarWithPreScrolledLazyColumn() { + val lazyListState = rememberLazyListState(initialFirstVisibleItemIndex = 30) + // Pass the state to ensure the top app bar color updates correctly when content is reversed or + // pre-scrolled. + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(lazyListState = lazyListState) + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + TopAppBar( + title = { Text("TopAppBar", maxLines = 1, overflow = TextOverflow.Ellipsis) }, + navigationIcon = { + TooltipBox( + positionProvider = + TooltipDefaults.rememberTooltipPositionProvider( + TooltipAnchorPosition.Above + ), + tooltip = { PlainTooltip { Text("Menu") } }, + state = rememberTooltipState(), + ) { + IconButton(onClick = { /* doSomething() */ }) { + Icon(imageVector = Icons.Filled.Menu, contentDescription = "Menu") + } + } + }, + scrollBehavior = scrollBehavior, + ) + }, + content = { innerPadding -> + LazyColumn( + state = lazyListState, + contentPadding = innerPadding, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + val list = (0..75).map { it.toString() } + items(count = list.size) { + Text( + text = list[it], + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + ) + } + } + }, + ) +} + +/** + * A sample for a pinned small [TopAppBar] that is scrolled with a reversed [LazyVerticalGrid]. + * + * The top app bar here is pinned to its location and changes its container color when the content + * under it is scrolled. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Sampled +@Composable +fun PinnedTopAppBarWithReversedLazyGrid() { + val lazyGridState = rememberLazyGridState() + // In a reversed grid, we need to provide a custom `isAtTopState` to the scroll behavior + // to ensure the top app bar's color changes correctly. + val isAtTopState = + remember(lazyGridState) { + derivedStateOf { + if (lazyGridState.layoutInfo.reverseLayout) { + !lazyGridState.canScrollForward + } else { + !lazyGridState.canScrollBackward + } + } + } + val scrollBehavior = + TopAppBarDefaults.pinnedScrollBehavior(isAtTopState = { isAtTopState.value }) + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + TopAppBar( + title = { Text("TopAppBar", maxLines = 1, overflow = TextOverflow.Ellipsis) }, + navigationIcon = { + TooltipBox( + positionProvider = + TooltipDefaults.rememberTooltipPositionProvider( + TooltipAnchorPosition.Above + ), + tooltip = { PlainTooltip { Text("Menu") } }, + state = rememberTooltipState(), + ) { + IconButton(onClick = { /* doSomething() */ }) { + Icon(imageVector = Icons.Filled.Menu, contentDescription = "Menu") + } + } + }, + scrollBehavior = scrollBehavior, + ) + }, + content = { innerPadding -> + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 100.dp), + reverseLayout = true, + contentPadding = innerPadding, + state = lazyGridState, + ) { + val list = (0..75).map { it.toString() } + items(count = list.size) { + Box(Modifier.fillMaxWidth().height(50.dp)) { + Text( + text = list[it], + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + ) + } + } + } + }, + ) +} + /** * A sample for a small [TopAppBar] that collapses when the content is scrolled up, and appears when * the content scrolled down. @@ -606,6 +740,63 @@ fun EnterAlwaysTopAppBar() { ) } +/** + * A sample for a small [TopAppBar] that collapses when the content is scrolled up, and appears when + * the content is scrolled down, using a [Column] with reverse scrolling. + */ +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) +@Preview +@Sampled +@Composable +fun EnterAlwaysTopAppBarWithReverseScrolling() { + val scrollState = rememberScrollState() + val scrollBehavior = + // Pass these parameters to ensure the top app bar color updates correctly when content has + // reverse scrolling. + TopAppBarDefaults.enterAlwaysScrollBehavior( + scrollState = scrollState, + reverseScrolling = true, + ) + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + TopAppBar( + title = { Text("TopAppBar", maxLines = 1, overflow = TextOverflow.Ellipsis) }, + navigationIcon = { + TooltipBox( + positionProvider = + TooltipDefaults.rememberTooltipPositionProvider( + TooltipAnchorPosition.Above + ), + tooltip = { PlainTooltip { Text("Menu") } }, + state = rememberTooltipState(), + ) { + IconButton(onClick = { /* doSomething() */ }) { + Icon(imageVector = Icons.Filled.Menu, contentDescription = "Menu") + } + } + }, + scrollBehavior = scrollBehavior, + ) + }, + content = { innerPadding -> + Column( + modifier = + Modifier.fillMaxSize() + .padding(paddingValues = innerPadding) + .verticalScroll(state = scrollState, reverseScrolling = true), + verticalArrangement = Arrangement.Bottom, + ) { + (0..75).forEach { + Box(modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp)) { + Text(text = it.toString(), style = MaterialTheme.typography.bodyLarge) + } + } + } + }, + ) +} + /** * A sample for a [MediumTopAppBar] that collapses when the content is scrolled up, and appears when * the content is completely scrolled back down. diff --git a/compose/material3/material3/src/androidDeviceTest/kotlin/androidx/compose/material3/AppBarTest.kt b/compose/material3/material3/src/androidDeviceTest/kotlin/androidx/compose/material3/AppBarTest.kt index 191c21bea7939..4f248f0227c50 100644 --- a/compose/material3/material3/src/androidDeviceTest/kotlin/androidx/compose/material3/AppBarTest.kt +++ b/compose/material3/material3/src/androidDeviceTest/kotlin/androidx/compose/material3/AppBarTest.kt @@ -39,6 +39,9 @@ import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -51,6 +54,8 @@ import androidx.compose.material3.tokens.AppBarTokens import androidx.compose.material3.tokens.BottomAppBarTokens import androidx.compose.material3.tokens.TypographyKeyTokens import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.remember import androidx.compose.testutils.assertContainsColor import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -2383,6 +2388,388 @@ class AppBarTest { rule.onNodeWithTag(TopAppBarTestTag).captureToImage().assertContainsColor(Color.Red) } + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) + @Test + fun topAppBar_pinned_changeColors_reverseLayout_preScrolledLazyGrid() { + lateinit var scrollBehavior: TopAppBarScrollBehavior + rule.setMaterialContent(lightColorScheme()) { + val lazyGridState = rememberLazyGridState(initialFirstVisibleItemIndex = 30) + val isAtTopState = + remember(lazyGridState) { + derivedStateOf { + if (lazyGridState.layoutInfo.reverseLayout) { + !lazyGridState.canScrollForward + } else { + !lazyGridState.canScrollBackward + } + } + } + scrollBehavior = + TopAppBarDefaults.pinnedScrollBehavior(isAtTopState = { isAtTopState.value }) + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + Box(Modifier.testTag(TopAppBarTestTag)) { + TopAppBar( + title = { Text("Title") }, + scrollBehavior = scrollBehavior, + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = Color.Red, + scrolledContainerColor = Color.Green, + ), + ) + } + }, + content = { contentPadding -> + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 100.dp), + contentPadding = contentPadding, + state = lazyGridState, + modifier = Modifier.testTag(LazyGridTestTag), + reverseLayout = true, + ) { + items(100) { Box(Modifier.fillMaxWidth().height(50.dp)) } + } + }, + ) + } + + rule.onNodeWithTag(TopAppBarTestTag).captureToImage().assertContainsColor(Color.Green) + + // Swipe down to scroll the content. + rule.onNodeWithTag(LazyGridTestTag).performTouchInput { + swipeDown(startY = 0f, endY = 500f) + } + rule.waitForIdle() + rule.onNodeWithTag(TopAppBarTestTag).captureToImage().assertContainsColor(Color.Green) + + // Swipe down to scroll the content and collapse the top app bar. + rule.onNodeWithTag(LazyGridTestTag).performTouchInput { + swipeDown(startY = 500f, endY = height + 1000f) + } + rule.waitForIdle() + rule.onNodeWithTag(TopAppBarTestTag).captureToImage().assertContainsColor(Color.Red) + } + + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) + @Test + fun topAppBar_pinned_changeColors_reverseLayout_scrolledLazyGrid() { + lateinit var scrollBehavior: TopAppBarScrollBehavior + rule.setMaterialContent(lightColorScheme()) { + val lazyGridState = rememberLazyGridState() + val isAtTopState = + remember(lazyGridState) { + derivedStateOf { + if (lazyGridState.layoutInfo.reverseLayout) { + !lazyGridState.canScrollForward + } else { + !lazyGridState.canScrollBackward + } + } + } + scrollBehavior = + TopAppBarDefaults.pinnedScrollBehavior(isAtTopState = { isAtTopState.value }) + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + Box(Modifier.testTag(TopAppBarTestTag)) { + TopAppBar( + title = { Text("Title") }, + scrollBehavior = scrollBehavior, + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = Color.Red, + scrolledContainerColor = Color.Green, + ), + ) + } + }, + ) { contentPadding -> + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 100.dp), + contentPadding = contentPadding, + state = lazyGridState, + modifier = Modifier.testTag(LazyGridTestTag), + reverseLayout = true, + ) { + items(100) { Box(Modifier.fillMaxWidth().height(50.dp)) } + } + } + } + + rule.onNodeWithTag(TopAppBarTestTag).captureToImage().assertContainsColor(Color.Green) + + // Swipe down to scroll the content. + rule.onNodeWithTag(LazyGridTestTag).performTouchInput { + swipeDown(startY = 0f, endY = 500f) + } + rule.waitForIdle() + rule.onNodeWithTag(TopAppBarTestTag).captureToImage().assertContainsColor(Color.Green) + + // Swipe down to scroll the content and collapse the top app bar. + rule.onNodeWithTag(LazyGridTestTag).performTouchInput { + swipeDown(startY = 500f, endY = height + 2000f) + } + rule.waitForIdle() + rule.onNodeWithTag(TopAppBarTestTag).captureToImage().assertContainsColor(Color.Red) + } + + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) + @Test + fun topAppBar_pinned_changeColors_scrolledLazyGrid() { + lateinit var scrollBehavior: TopAppBarScrollBehavior + rule.setMaterialContent(lightColorScheme()) { + val lazyGridState = rememberLazyGridState() + val isAtTopState = + remember(lazyGridState) { + derivedStateOf { + if (lazyGridState.layoutInfo.reverseLayout) { + !lazyGridState.canScrollForward + } else { + !lazyGridState.canScrollBackward + } + } + } + scrollBehavior = + TopAppBarDefaults.pinnedScrollBehavior(isAtTopState = { isAtTopState.value }) + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + Box(Modifier.testTag(TopAppBarTestTag)) { + TopAppBar( + title = { Text("Title") }, + scrollBehavior = scrollBehavior, + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = Color.Red, + scrolledContainerColor = Color.Green, + ), + ) + } + }, + ) { contentPadding -> + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 100.dp), + contentPadding = contentPadding, + state = lazyGridState, + modifier = Modifier.testTag(LazyGridTestTag), + ) { + items(100) { Box(Modifier.fillMaxWidth().height(50.dp)) } + } + } + } + + rule.onNodeWithTag(TopAppBarTestTag).captureToImage().assertContainsColor(Color.Red) + + // Swipe up to scroll the content. + rule.onNodeWithTag(LazyGridTestTag).performTouchInput { + swipeUp(startY = height - 200f, endY = height - 1000f) + } + rule.waitForIdle() + rule.onNodeWithTag(TopAppBarTestTag).captureToImage().assertContainsColor(Color.Green) + } + + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) + @Test + fun topAppBar_enterAlways_changeColors_scrolledLazyGrid() { + lateinit var scrollBehavior: TopAppBarScrollBehavior + rule.setMaterialContent(lightColorScheme()) { + val lazyGridState = rememberLazyGridState() + val isAtTopState = + remember(lazyGridState) { + derivedStateOf { + if (lazyGridState.layoutInfo.reverseLayout) { + !lazyGridState.canScrollForward + } else { + !lazyGridState.canScrollBackward + } + } + } + scrollBehavior = + TopAppBarDefaults.enterAlwaysScrollBehavior(isAtTopState = { isAtTopState.value }) + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + Box(Modifier.testTag(TopAppBarTestTag)) { + TopAppBar( + title = { Text("Title") }, + scrollBehavior = scrollBehavior, + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = Color.Red, + scrolledContainerColor = Color.Green, + ), + ) + } + }, + content = { contentPadding -> + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 100.dp), + contentPadding = contentPadding, + state = lazyGridState, + modifier = Modifier.testTag(LazyGridTestTag), + ) { + items(100) { Box(Modifier.fillMaxWidth().height(50.dp)) } + } + }, + ) + } + + rule.onNodeWithTag(TopAppBarTestTag).captureToImage().assertContainsColor(Color.Red) + + // Swipe up to scroll the content and collapse the top app bar. + rule.onNodeWithTag(LazyGridTestTag).performTouchInput { + swipeUp(startY = height - 200f, endY = height - 1000f) + } + rule.waitForIdle() + rule.onNodeWithTag(TopAppBarTestTag).isNotDisplayed() + + rule.onNodeWithTag(LazyGridTestTag).performTouchInput { + swipeDown(startY = height - 1000f, endY = height - 800f) + } + rule.waitForIdle() + rule.onNodeWithTag(TopAppBarTestTag).captureToImage().assertContainsColor(Color.Green) + } + + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) + @Test + fun topAppBar_enterAlways_changeColors_reversedLayout_scrolledLazyGrid() { + lateinit var scrollBehavior: TopAppBarScrollBehavior + rule.setMaterialContent(lightColorScheme()) { + val lazyGridState = rememberLazyGridState() + val isAtTopState = + remember(lazyGridState) { + derivedStateOf { + if (lazyGridState.layoutInfo.reverseLayout) { + !lazyGridState.canScrollForward + } else { + !lazyGridState.canScrollBackward + } + } + } + scrollBehavior = + TopAppBarDefaults.enterAlwaysScrollBehavior(isAtTopState = { isAtTopState.value }) + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + Box(modifier = Modifier.testTag(TopAppBarTestTag)) { + TopAppBar( + title = { Text("Title") }, + scrollBehavior = scrollBehavior, + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = Color.Red, + scrolledContainerColor = Color.Green, + ), + ) + } + }, + content = { contentPadding -> + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 100.dp), + contentPadding = contentPadding, + state = lazyGridState, + modifier = Modifier.testTag(LazyGridTestTag), + reverseLayout = true, + ) { + items(100) { index -> + Box(modifier = Modifier.height(100.dp).background(Color.LightGray)) { + Text( + text = "Item $index", + modifier = Modifier.align(Alignment.Center), + ) + } + } + } + }, + ) + } + + rule.onNodeWithTag(TopAppBarTestTag).captureToImage().assertContainsColor(Color.Green) + rule.onNodeWithTag(LazyGridTestTag).performTouchInput { + swipeDown(startY = height - 1000f, endY = height - 500f, 150) + } + rule.waitForIdle() + rule.onNodeWithTag(TopAppBarTestTag).captureToImage().assertContainsColor(Color.Green) + rule.onNodeWithTag(LazyGridTestTag).performTouchInput { + swipeUp(startY = height - 500f, endY = height - 1000f, 100) + } + rule.waitForIdle() + rule.onNodeWithTag(TopAppBarTestTag).captureToImage().assertContainsColor(Color.Green) + + // Swipe down to scroll the content and collapse the top app bar. + rule.onNodeWithTag(LazyGridTestTag).performTouchInput { + swipeDown(startY = 500f, endY = height + 1000f) + } + rule.waitForIdle() + rule.onNodeWithTag(TopAppBarTestTag).captureToImage().assertContainsColor(Color.Red) + } + + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) + @Test + fun topAppBar_enterAlways_changeColors_reverseLayout_preScrolledLazyGrid() { + lateinit var scrollBehavior: TopAppBarScrollBehavior + rule.setMaterialContent(lightColorScheme()) { + val lazyGridState = rememberLazyGridState(initialFirstVisibleItemIndex = 30) + val isAtTopState = + remember(lazyGridState) { + derivedStateOf { + if (lazyGridState.layoutInfo.reverseLayout) { + !lazyGridState.canScrollForward + } else { + !lazyGridState.canScrollBackward + } + } + } + scrollBehavior = + TopAppBarDefaults.enterAlwaysScrollBehavior(isAtTopState = { isAtTopState.value }) + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + Box(Modifier.testTag(TopAppBarTestTag)) { + TopAppBar( + title = { Text("Title") }, + scrollBehavior = scrollBehavior, + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = Color.Red, + scrolledContainerColor = Color.Green, + ), + ) + } + }, + content = { contentPadding -> + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 100.dp), + contentPadding = contentPadding, + state = lazyGridState, + modifier = Modifier.testTag(LazyGridTestTag), + reverseLayout = true, + ) { + items(100) { Box(Modifier.fillMaxWidth().height(50.dp)) } + } + }, + ) + } + + rule.onNodeWithTag(TopAppBarTestTag).captureToImage().assertContainsColor(Color.Green) + + // Swipe down to scroll the content. + rule.onNodeWithTag(LazyGridTestTag).performTouchInput { + swipeDown(startY = 0f, endY = 500f) + } + rule.waitForIdle() + rule.onNodeWithTag(TopAppBarTestTag).captureToImage().assertContainsColor(Color.Green) + + // Swipe down to scroll the content and collapse the top app bar. + rule.onNodeWithTag(LazyGridTestTag).performTouchInput { + swipeDown(startY = 500f, endY = height + 1000f) + } + rule.waitForIdle() + rule.onNodeWithTag(TopAppBarTestTag).captureToImage().assertContainsColor(Color.Red) + } + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) @Test fun topAppBar_enterAlways_changeColors_scrolledColumn_setIsAtTop() { @@ -3469,4 +3856,5 @@ class AppBarTest { private val RowTestTag = "row" private val BoxTestTag = "BoxTestTag" private val ScrollableContentTestTag = "ScrollableContentTestTag" + private val LazyGridTestTag = "LazyGridTestTag" } diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt index af76ac0866c8c..c9b97d1f354cd 100644 --- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt +++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt @@ -200,7 +200,10 @@ fun TopAppBar( * [scrollBehavior] to customize its nested scrolling behavior when working in conjunction with a * scrolling content looks like: * @sample androidx.compose.material3.samples.PinnedTopAppBar + * @sample androidx.compose.material3.samples.PinnedTopAppBarWithPreScrolledLazyColumn + * @sample androidx.compose.material3.samples.PinnedTopAppBarWithReversedLazyGrid * @sample androidx.compose.material3.samples.EnterAlwaysTopAppBar + * @sample androidx.compose.material3.samples.EnterAlwaysTopAppBarWithReverseScrolling * @param title the title to be displayed in the top app bar * @param modifier the [Modifier] to be applied to this top app bar * @param navigationIcon the navigation icon displayed at the start of the top app bar. This should @@ -1679,13 +1682,11 @@ object TopAppBarDefaults { canScroll: () -> Boolean = { true }, ): TopAppBarScrollBehavior { val isAtTopState = rememberIsAtTop(lazyListState = lazyListState) - return remember(state, canScroll, isAtTopState) { - PinnedScrollBehavior( - state = state, - canScroll = canScroll, - isAtTopState = { isAtTopState.value }, - ) - } + return pinnedScrollBehavior( + state = state, + canScroll = canScroll, + isAtTopState = { isAtTopState.value }, + ) } /** @@ -1712,12 +1713,34 @@ object TopAppBarDefaults { ): TopAppBarScrollBehavior { val isAtTopState = rememberIsAtTop(scrollState = scrollState, reverseScrolling = reverseScrolling) + return pinnedScrollBehavior( + state = state, + canScroll = canScroll, + isAtTopState = { isAtTopState.value }, + ) + } + + /** + * Returns a pinned [TopAppBarScrollBehavior] that tracks nested-scroll callbacks and updates + * its [TopAppBarState.contentOffset] accordingly. + * + * The returned [TopAppBarScrollBehavior] is remembered across compositions. + * + * @param state the state object to be used to control or observe the top app bar's scroll + * state. See [rememberTopAppBarState] for a state that is remembered across compositions + * @param canScroll a callback used to determine whether scroll events are to be handled by this + * pinned [TopAppBarScrollBehavior] + * @param isAtTopState state that indicates whether the content is scrolled to the top + */ + @ExperimentalMaterial3Api + @Composable + fun pinnedScrollBehavior( + state: TopAppBarState = rememberTopAppBarState(), + canScroll: () -> Boolean = { true }, + isAtTopState: () -> Boolean = { true }, + ): TopAppBarScrollBehavior { return remember(state, canScroll, isAtTopState) { - PinnedScrollBehavior( - state = state, - canScroll = canScroll, - isAtTopState = { isAtTopState.value }, - ) + PinnedScrollBehavior(state = state, canScroll = canScroll, isAtTopState = isAtTopState) } } @@ -1836,15 +1859,13 @@ object TopAppBarDefaults { flingAnimationSpec: DecayAnimationSpec? = rememberSplineBasedDecay(), ): TopAppBarScrollBehavior { val isAtTopState = rememberIsAtTop(lazyListState = lazyListState) - return remember(state, canScroll, snapAnimationSpec, flingAnimationSpec, isAtTopState) { - EnterAlwaysScrollBehavior( - state = state, - snapAnimationSpec = snapAnimationSpec, - flingAnimationSpec = flingAnimationSpec, - canScroll = canScroll, - isAtTopState = { isAtTopState.value }, - ) - } + return enterAlwaysScrollBehavior( + state = state, + canScroll = canScroll, + snapAnimationSpec = snapAnimationSpec, + flingAnimationSpec = flingAnimationSpec, + isAtTopState = { isAtTopState.value }, + ) } // TODO: Load the motionScheme tokens from the component tokens file @@ -1879,16 +1900,54 @@ object TopAppBarDefaults { flingAnimationSpec: DecayAnimationSpec? = rememberSplineBasedDecay(), ): TopAppBarScrollBehavior { val isAtTopState = rememberIsAtTop(scrollState, reverseScrolling) - return remember(state, canScroll, snapAnimationSpec, flingAnimationSpec, isAtTopState) { + return enterAlwaysScrollBehavior( + state = state, + canScroll = canScroll, + snapAnimationSpec = snapAnimationSpec, + flingAnimationSpec = flingAnimationSpec, + isAtTopState = { isAtTopState.value }, + ) + } + + // TODO: Load the motionScheme tokens from the component tokens file + /** + * Returns a [TopAppBarScrollBehavior]. A top app bar that is set up with this + * [TopAppBarScrollBehavior] will immediately collapse when the content is pulled up, and will + * immediately appear when the content is pulled down. Note: If your layout utilizes + * `reverseLayout` with [LazyListState] or involves `reverseScrolling` with [ScrollState], + * consider using other overloads that are specifically designed for these use cases. + * + * The returned [TopAppBarScrollBehavior] is remembered across compositions. + * + * @param state the state object to be used to control or observe the top app bar's scroll + * state. See [rememberTopAppBarState] for a state that is remembered across compositions. + * @param canScroll a callback used to determine whether scroll events are to be handled by this + * [TopAppBarScrollBehavior] + * @param snapAnimationSpec an optional [AnimationSpec] that defines how the top app bar snaps + * to either fully collapsed or fully extended state when a fling or a drag scrolled it into + * an intermediate position + * @param flingAnimationSpec an optional [DecayAnimationSpec] that defined how to fling the top + * app bar when the user flings the app bar itself, or the content below it + * @param isAtTopState state that indicates whether the content is scrolled to the top + */ + @ExperimentalMaterial3Api + @Composable + fun enterAlwaysScrollBehavior( + state: TopAppBarState = rememberTopAppBarState(), + canScroll: () -> Boolean = { true }, + snapAnimationSpec: AnimationSpec? = MotionSchemeKeyTokens.DefaultEffects.value(), + flingAnimationSpec: DecayAnimationSpec? = rememberSplineBasedDecay(), + isAtTopState: () -> Boolean = { true }, + ): TopAppBarScrollBehavior = + remember(state, canScroll, snapAnimationSpec, flingAnimationSpec) { EnterAlwaysScrollBehavior( state = state, snapAnimationSpec = snapAnimationSpec, flingAnimationSpec = flingAnimationSpec, canScroll = canScroll, - isAtTopState = { isAtTopState.value }, + isAtTopState = isAtTopState, ) } - } // TODO: Load the motionScheme tokens from the component tokens file /** @@ -3642,10 +3701,7 @@ private class EnterAlwaysScrollBehavior( } override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { - if ( - available.y > 0f && - (state.heightOffset == 0f || state.heightOffset == state.heightOffsetLimit) - ) { + if (available.y > 0f) { // Reset the total content offset to zero when scrolling all the way down. // This will eliminate some float precision inaccuracies. state.contentOffset = 0f From dacb10c8852ab61bc0ff8016727caa0979b75a3e Mon Sep 17 00:00:00 2001 From: androidx-jetpad Date: Fri, 6 Feb 2026 09:59:49 -0800 Subject: [PATCH 19/21] Updating docs-public/build.gradle Change-Id: Idb3aa0987abbe62916fef980de3ee4cfe0d288e2 --- docs-public/build.gradle | 246 +++++++++++++++++++-------------------- 1 file changed, 123 insertions(+), 123 deletions(-) diff --git a/docs-public/build.gradle b/docs-public/build.gradle index f566ceb94a6f4..2e7dd8bdff843 100644 --- a/docs-public/build.gradle +++ b/docs-public/build.gradle @@ -18,7 +18,7 @@ dependencies { docs("androidx.activity:activity-compose:1.13.0-alpha01") docs("androidx.activity:activity-ktx:1.13.0-alpha01") kmpDocs("androidx.annotation:annotation:1.9.1") - docs("androidx.annotation:annotation-experimental:1.6.0-alpha01") + docs("androidx.annotation:annotation-experimental:1.6.0-rc01") docs("androidx.appcompat:appcompat:1.7.1") docs("androidx.appcompat:appcompat-resources:1.7.1") docs("androidx.appfunctions:appfunctions:1.0.0-alpha07") @@ -36,84 +36,84 @@ dependencies { docs("androidx.asynclayoutinflater:asynclayoutinflater:1.1.0") docs("androidx.asynclayoutinflater:asynclayoutinflater-appcompat:1.1.0") docs("androidx.autofill:autofill:1.3.0") - docs("androidx.benchmark:benchmark-common:1.5.0-alpha02") - docs("androidx.benchmark:benchmark-junit4:1.5.0-alpha02") - docs("androidx.benchmark:benchmark-macro:1.5.0-alpha02") - docs("androidx.benchmark:benchmark-macro-junit4:1.5.0-alpha02") - kmpDocs("androidx.benchmark:benchmark-traceprocessor:1.5.0-alpha02") + docs("androidx.benchmark:benchmark-common:1.5.0-alpha03") + docs("androidx.benchmark:benchmark-junit4:1.5.0-alpha03") + docs("androidx.benchmark:benchmark-macro:1.5.0-alpha03") + docs("androidx.benchmark:benchmark-macro-junit4:1.5.0-alpha03") + kmpDocs("androidx.benchmark:benchmark-traceprocessor:1.5.0-alpha03") docs("androidx.biometric:biometric:1.4.0-alpha05") docs("androidx.biometric:biometric-compose:1.4.0-alpha05") - docs("androidx.browser:browser:1.10.0-alpha02") - docs("androidx.camera.featurecombinationquery:featurecombinationquery:1.6.0-beta01") - docs("androidx.camera.featurecombinationquery:featurecombinationquery-play-services:1.6.0-beta01") + docs("androidx.browser:browser:1.10.0-alpha03") + docs("androidx.camera.featurecombinationquery:featurecombinationquery:1.6.0-beta02") + docs("androidx.camera.featurecombinationquery:featurecombinationquery-play-services:1.6.0-beta02") docs("androidx.camera.media3:media3-effect:1.0.0-alpha04") - docs("androidx.camera.viewfinder:viewfinder-compose:1.6.0-beta01") - docs("androidx.camera.viewfinder:viewfinder-core:1.6.0-beta01") - docs("androidx.camera.viewfinder:viewfinder-view:1.6.0-beta01") + docs("androidx.camera.viewfinder:viewfinder-compose:1.6.0-beta02") + docs("androidx.camera.viewfinder:viewfinder-core:1.6.0-beta02") + docs("androidx.camera.viewfinder:viewfinder-view:1.6.0-beta02") docs("androidx.camera:camera-camera2:1.5.3") - docs("androidx.camera:camera-compose:1.6.0-beta01") - docs("androidx.camera:camera-core:1.6.0-beta01") - docs("androidx.camera:camera-effects:1.6.0-beta01") - docs("androidx.camera:camera-extensions:1.6.0-beta01") + docs("androidx.camera:camera-compose:1.6.0-beta02") + docs("androidx.camera:camera-core:1.6.0-beta02") + docs("androidx.camera:camera-effects:1.6.0-beta02") + docs("androidx.camera:camera-extensions:1.6.0-beta02") stubs(fileTree(dir: "../camera/camera-extensions-stub", include: ["camera-extensions-stub.jar"])) - docs("androidx.camera:camera-lifecycle:1.6.0-beta01") - docs("androidx.camera:camera-mlkit-vision:1.6.0-beta01") - docs("androidx.camera:camera-video:1.6.0-beta01") - docs("androidx.camera:camera-view:1.6.0-beta01") + docs("androidx.camera:camera-lifecycle:1.6.0-beta02") + docs("androidx.camera:camera-mlkit-vision:1.6.0-beta02") + docs("androidx.camera:camera-video:1.6.0-beta02") + docs("androidx.camera:camera-view:1.6.0-beta02") docs("androidx.car.app:app:1.8.0-alpha03") docs("androidx.car.app:app-automotive:1.8.0-alpha03") docs("androidx.car.app:app-projected:1.8.0-alpha03") docs("androidx.car.app:app-testing:1.8.0-alpha03") docs("androidx.cardview:cardview:1.0.0") - kmpDocs("androidx.collection:collection:1.6.0-beta01") - docs("androidx.collection:collection-ktx:1.6.0-beta01") - kmpDocs("androidx.compose.animation:animation:1.11.0-alpha04") - kmpDocs("androidx.compose.animation:animation-core:1.11.0-alpha04") - kmpDocs("androidx.compose.animation:animation-graphics:1.11.0-alpha04") - kmpDocs("androidx.compose.foundation:foundation:1.11.0-alpha04") - kmpDocs("androidx.compose.foundation:foundation-layout:1.11.0-alpha04") - kmpDocs("androidx.compose.material3.adaptive:adaptive:1.3.0-alpha06") - kmpDocs("androidx.compose.material3.adaptive:adaptive-layout:1.3.0-alpha06") - kmpDocs("androidx.compose.material3.adaptive:adaptive-navigation:1.3.0-alpha06") - kmpDocs("androidx.compose.material3.adaptive:adaptive-navigation3:1.3.0-alpha06") - kmpDocs("androidx.compose.material3:material3:1.5.0-alpha13") - kmpDocs("androidx.compose.material3:material3-adaptive-navigation-suite:1.5.0-alpha13") - kmpDocs("androidx.compose.material3:material3-window-size-class:1.5.0-alpha13") - kmpDocs("androidx.compose.material:material:1.11.0-alpha04") + kmpDocs("androidx.collection:collection:1.6.0-rc01") + docs("androidx.collection:collection-ktx:1.6.0-rc01") + kmpDocs("androidx.compose.animation:animation:1.11.0-alpha05") + kmpDocs("androidx.compose.animation:animation-core:1.11.0-alpha05") + kmpDocs("androidx.compose.animation:animation-graphics:1.11.0-alpha05") + kmpDocs("androidx.compose.foundation:foundation:1.11.0-alpha05") + kmpDocs("androidx.compose.foundation:foundation-layout:1.11.0-alpha05") + kmpDocs("androidx.compose.material3.adaptive:adaptive:1.3.0-alpha08") + kmpDocs("androidx.compose.material3.adaptive:adaptive-layout:1.3.0-alpha08") + kmpDocs("androidx.compose.material3.adaptive:adaptive-navigation:1.3.0-alpha08") + kmpDocs("androidx.compose.material3.adaptive:adaptive-navigation3:1.3.0-alpha08") + kmpDocs("androidx.compose.material3:material3:1.5.0-alpha14") + kmpDocs("androidx.compose.material3:material3-adaptive-navigation-suite:1.5.0-alpha14") + kmpDocs("androidx.compose.material3:material3-window-size-class:1.5.0-alpha14") + kmpDocs("androidx.compose.material:material:1.11.0-alpha05") kmpDocs("androidx.compose.material:material-icons-core:1.7.8") - docs("androidx.compose.material:material-navigation:1.11.0-alpha04") - kmpDocs("androidx.compose.material:material-ripple:1.11.0-alpha04") - docs("androidx.compose.remote:remote-core:1.0.0-alpha03") - kmpDocs("androidx.compose.remote:remote-creation:1.0.0-alpha03") - docs("androidx.compose.remote:remote-creation-compose:1.0.0-alpha03") - docs("androidx.compose.remote:remote-creation-core:1.0.0-alpha03") - docs("androidx.compose.remote:remote-player-compose:1.0.0-alpha03") - docs("androidx.compose.remote:remote-player-core:1.0.0-alpha03") - docs("androidx.compose.remote:remote-player-view:1.0.0-alpha03") - docs("androidx.compose.remote:remote-tooling-preview:1.0.0-alpha03") - kmpDocs("androidx.compose.runtime:runtime:1.11.0-alpha04") - kmpDocs("androidx.compose.runtime:runtime-annotation:1.11.0-alpha04") - docs("androidx.compose.runtime:runtime-livedata:1.11.0-alpha04") - kmpDocs("androidx.compose.runtime:runtime-retain:1.11.0-alpha04") - kmpDocs("androidx.compose.runtime:runtime-rxjava2:1.11.0-alpha04") - kmpDocs("androidx.compose.runtime:runtime-rxjava3:1.11.0-alpha04") - kmpDocs("androidx.compose.runtime:runtime-saveable:1.11.0-alpha04") - docs("androidx.compose.runtime:runtime-tracing:1.11.0-alpha04") - kmpDocs("androidx.compose.ui:ui:1.11.0-alpha04") - kmpDocs("androidx.compose.ui:ui-geometry:1.11.0-alpha04") - kmpDocs("androidx.compose.ui:ui-graphics:1.11.0-alpha04") - kmpDocs("androidx.compose.ui:ui-test:1.11.0-alpha04") - kmpDocs("androidx.compose.ui:ui-test-accessibility:1.11.0-alpha04") - kmpDocs("androidx.compose.ui:ui-test-junit4:1.11.0-alpha04") - kmpDocs("androidx.compose.ui:ui-test-junit4-accessibility:1.11.0-alpha04") - kmpDocs("androidx.compose.ui:ui-text:1.11.0-alpha04") - docs("androidx.compose.ui:ui-text-google-fonts:1.11.0-alpha04") - kmpDocs("androidx.compose.ui:ui-tooling:1.11.0-alpha04") - kmpDocs("androidx.compose.ui:ui-tooling-data:1.11.0-alpha04") - kmpDocs("androidx.compose.ui:ui-tooling-preview:1.11.0-alpha04") - kmpDocs("androidx.compose.ui:ui-unit:1.11.0-alpha04") - kmpDocs("androidx.compose.ui:ui-util:1.11.0-alpha04") - docs("androidx.compose.ui:ui-viewbinding:1.11.0-alpha04") + docs("androidx.compose.material:material-navigation:1.11.0-alpha05") + kmpDocs("androidx.compose.material:material-ripple:1.11.0-alpha05") + docs("androidx.compose.remote:remote-core:1.0.0-alpha04") + kmpDocs("androidx.compose.remote:remote-creation:1.0.0-alpha04") + docs("androidx.compose.remote:remote-creation-compose:1.0.0-alpha04") + docs("androidx.compose.remote:remote-creation-core:1.0.0-alpha04") + docs("androidx.compose.remote:remote-player-compose:1.0.0-alpha04") + docs("androidx.compose.remote:remote-player-core:1.0.0-alpha04") + docs("androidx.compose.remote:remote-player-view:1.0.0-alpha04") + docs("androidx.compose.remote:remote-tooling-preview:1.0.0-alpha04") + kmpDocs("androidx.compose.runtime:runtime:1.11.0-alpha05") + kmpDocs("androidx.compose.runtime:runtime-annotation:1.11.0-alpha05") + docs("androidx.compose.runtime:runtime-livedata:1.11.0-alpha05") + kmpDocs("androidx.compose.runtime:runtime-retain:1.11.0-alpha05") + kmpDocs("androidx.compose.runtime:runtime-rxjava2:1.11.0-alpha05") + kmpDocs("androidx.compose.runtime:runtime-rxjava3:1.11.0-alpha05") + kmpDocs("androidx.compose.runtime:runtime-saveable:1.11.0-alpha05") + docs("androidx.compose.runtime:runtime-tracing:1.11.0-alpha05") + kmpDocs("androidx.compose.ui:ui:1.11.0-alpha05") + kmpDocs("androidx.compose.ui:ui-geometry:1.11.0-alpha05") + kmpDocs("androidx.compose.ui:ui-graphics:1.11.0-alpha05") + kmpDocs("androidx.compose.ui:ui-test:1.11.0-alpha05") + kmpDocs("androidx.compose.ui:ui-test-accessibility:1.11.0-alpha05") + kmpDocs("androidx.compose.ui:ui-test-junit4:1.11.0-alpha05") + kmpDocs("androidx.compose.ui:ui-test-junit4-accessibility:1.11.0-alpha05") + kmpDocs("androidx.compose.ui:ui-text:1.11.0-alpha05") + docs("androidx.compose.ui:ui-text-google-fonts:1.11.0-alpha05") + kmpDocs("androidx.compose.ui:ui-tooling:1.11.0-alpha05") + kmpDocs("androidx.compose.ui:ui-tooling-data:1.11.0-alpha05") + kmpDocs("androidx.compose.ui:ui-tooling-preview:1.11.0-alpha05") + kmpDocs("androidx.compose.ui:ui-unit:1.11.0-alpha05") + kmpDocs("androidx.compose.ui:ui-util:1.11.0-alpha05") + docs("androidx.compose.ui:ui-viewbinding:1.11.0-alpha05") docs("androidx.concurrent:concurrent-futures:1.3.0") docs("androidx.concurrent:concurrent-futures-ktx:1.3.0") docs("androidx.constraintlayout:constraintlayout:2.2.1") @@ -121,23 +121,23 @@ dependencies { docs("androidx.constraintlayout:constraintlayout-core:1.1.1") docs("androidx.contentpager:contentpager:1.0.0") docs("androidx.coordinatorlayout:coordinatorlayout:1.3.0") - docs("androidx.core:core:1.18.0-alpha01") + docs("androidx.core:core:1.18.0-rc01") docs("androidx.core:core-animation:1.0.0") docs("androidx.core:core-animation-testing:1.0.0") docs("androidx.core:core-backported-fixes:1.0.0") docs("androidx.core:core-google-shortcuts:1.2.0-alpha01") docs("androidx.core:core-i18n:1.0.0") - docs("androidx.core:core-ktx:1.18.0-alpha01") + docs("androidx.core:core-ktx:1.18.0-rc01") docs("androidx.core:core-location-altitude:1.0.0-beta01") docs("androidx.core:core-performance:1.0.0") docs("androidx.core:core-performance-play-services:1.0.0") docs("androidx.core:core-performance-testing:1.0.0") - docs("androidx.core:core-pip:1.0.0-alpha01") + docs("androidx.core:core-pip:1.0.0-alpha02") docs("androidx.core:core-remoteviews:1.1.0") docs("androidx.core:core-role:1.2.0-alpha01") docs("androidx.core:core-splashscreen:1.2.0") - docs("androidx.core:core-telecom:1.1.0-alpha02") - docs("androidx.core:core-testing:1.18.0-alpha01") + docs("androidx.core:core-telecom:1.1.0-alpha03") + docs("androidx.core:core-testing:1.18.0-rc01") docs("androidx.core.uwb:uwb:1.0.0-alpha11") docs("androidx.core.uwb:uwb-rxjava3:1.0.0-alpha11") docs("androidx.core:core-viewtree:1.0.0") @@ -184,8 +184,8 @@ dependencies { docs("androidx.fragment:fragment-compose:1.8.9") docs("androidx.fragment:fragment-ktx:1.8.9") docs("androidx.fragment:fragment-testing:1.8.9") - docs("androidx.glance.wear:wear:1.0.0-alpha02") - docs("androidx.glance.wear:wear-core:1.0.0-alpha02") + docs("androidx.glance.wear:wear:1.0.0-alpha03") + docs("androidx.glance.wear:wear-core:1.0.0-alpha03") docs("androidx.glance:glance:1.2.0-rc01") docs("androidx.glance:glance-appwidget:1.2.0-rc01") docs("androidx.glance:glance-appwidget-multiprocess:1.2.0-rc01") @@ -202,7 +202,7 @@ dependencies { docs("androidx.gridlayout:gridlayout:1.1.0") docs("androidx.health.connect:connect-client:1.2.0-alpha02") docs("androidx.health.connect:connect-testing:1.0.0-alpha03") - docs("androidx.health:health-services-client:1.1.0-alpha05") + docs("androidx.health:health-services-client:1.1.0-beta01") docs("androidx.heifwriter:heifwriter:1.2.0-alpha01") docs("androidx.hilt:hilt-common:1.3.0") docs("androidx.hilt:hilt-lifecycle-viewmodel:1.3.0") @@ -284,10 +284,10 @@ dependencies { docsWithoutApiSince("androidx.media3:media3-ui-compose:1.10.0-alpha01") docsWithoutApiSince("androidx.media3:media3-ui-compose-material3:1.10.0-alpha01") docsWithoutApiSince("androidx.media3:media3-ui-leanback:1.10.0-alpha01") - docs("androidx.mediarouter:mediarouter:1.8.1") - docs("androidx.mediarouter:mediarouter-testing:1.8.1") + docs("androidx.mediarouter:mediarouter:1.9.0-alpha01") + docs("androidx.mediarouter:mediarouter-testing:1.9.0-alpha01") docs("androidx.metrics:metrics-performance:1.0.0") - kmpDocs("androidx.navigation3:navigation3-ui:1.1.0-alpha03") + kmpDocs("androidx.navigation3:navigation3-ui:1.1.0-alpha04") kmpDocs("androidx.navigation:navigation-common:2.9.7") docs("androidx.navigation:navigation-common-ktx:2.9.7") kmpDocs("androidx.navigation:navigation-compose:2.9.7") @@ -301,27 +301,27 @@ dependencies { kmpDocs("androidx.navigation:navigation-testing:2.9.7") docs("androidx.navigation:navigation-ui:2.9.7") docs("androidx.navigation:navigation-ui-ktx:2.9.7") - kmpDocs("androidx.navigation3:navigation3-runtime:1.1.0-alpha03") + kmpDocs("androidx.navigation3:navigation3-runtime:1.1.0-alpha04") kmpDocs("androidx.navigationevent:navigationevent:1.0.2") kmpDocs("androidx.navigationevent:navigationevent-compose:1.0.2") kmpDocs("androidx.navigationevent:navigationevent-testing:1.0.2") - kmpDocs("androidx.paging:paging-common:3.4.0") - docs("androidx.paging:paging-common-ktx:3.4.0") - kmpDocs("androidx.paging:paging-compose:3.4.0") - docs("androidx.paging:paging-guava:3.4.0") - docs("androidx.paging:paging-runtime:3.4.0") - docs("androidx.paging:paging-runtime-ktx:3.4.0") - docs("androidx.paging:paging-rxjava2:3.4.0") - docs("androidx.paging:paging-rxjava2-ktx:3.4.0") - docs("androidx.paging:paging-rxjava3:3.4.0") - kmpDocs("androidx.paging:paging-testing:3.4.0") + kmpDocs("androidx.paging:paging-common:3.4.1") + docs("androidx.paging:paging-common-ktx:3.4.1") + kmpDocs("androidx.paging:paging-compose:3.4.1") + docs("androidx.paging:paging-guava:3.4.1") + docs("androidx.paging:paging-runtime:3.4.1") + docs("androidx.paging:paging-runtime-ktx:3.4.1") + docs("androidx.paging:paging-rxjava2:3.4.1") + docs("androidx.paging:paging-rxjava2-ktx:3.4.1") + docs("androidx.paging:paging-rxjava3:3.4.1") + kmpDocs("androidx.paging:paging-testing:3.4.1") docs("androidx.palette:palette:1.0.0") docs("androidx.palette:palette-ktx:1.0.0") - docs("androidx.pdf:pdf-compose:1.0.0-alpha12") - docs("androidx.pdf:pdf-document-service:1.0.0-alpha12") - docs("androidx.pdf:pdf-ink:1.0.0-alpha12") - docs("androidx.pdf:pdf-viewer:1.0.0-alpha12") - docs("androidx.pdf:pdf-viewer-fragment:1.0.0-alpha12") + docs("androidx.pdf:pdf-compose:1.0.0-alpha13") + docs("androidx.pdf:pdf-document-service:1.0.0-alpha13") + docs("androidx.pdf:pdf-ink:1.0.0-alpha13") + docs("androidx.pdf:pdf-viewer:1.0.0-alpha13") + docs("androidx.pdf:pdf-viewer-fragment:1.0.0-alpha13") docs("androidx.percentlayout:percentlayout:1.0.1") docs("androidx.photopicker:photopicker:1.0.0-alpha01") docs("androidx.photopicker:photopicker-compose:1.0.0-alpha01") @@ -370,8 +370,8 @@ dependencies { docs("androidx.security:security-app-authenticator-testing:1.0.0") docs("androidx.security:security-crypto:1.1.0") docs("androidx.security:security-crypto-ktx:1.1.0") - docs("androidx.security:security-state:1.0.0-beta01") - docs("androidx.security:security-state-provider:1.0.0-alpha01") + docs("androidx.security:security-state:1.1.0-alpha01") + docs("androidx.security:security-state-provider:1.0.0-alpha02") docs("androidx.sharetarget:sharetarget:1.2.0") docs("androidx.slice:slice-builders:1.1.0-alpha02") docs("androidx.slice:slice-builders-ktx:1.0.0-alpha08") @@ -385,7 +385,7 @@ dependencies { docs("androidx.startup:startup-runtime:1.2.0") docs("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0") // androidx.test is not hosted in androidx - kmpDocs("androidx.test.uiautomator:uiautomator-shell:1.0.0-alpha03") + kmpDocs("androidx.test.uiautomator:uiautomator-shell:2.4.0-beta01") docsWithoutApiSince("androidx.test:core:1.7.0") docsWithoutApiSince("androidx.test:core-ktx:1.7.0") docsWithoutApiSince("androidx.test:monitor:1.8.0") @@ -405,7 +405,7 @@ dependencies { docsWithoutApiSince("androidx.test.ext:junit-ktx:1.3.0") docsWithoutApiSince("androidx.test.ext:truth:1.7.0") docsWithoutApiSince("androidx.test.services:storage:1.6.0") - docs("androidx.test.uiautomator:uiautomator:2.4.0-alpha07") + docs("androidx.test.uiautomator:uiautomator:2.4.0-beta01") docs("androidx.text:text-vertical:1.0.0-alpha02") kmpDocs("androidx.tracing:tracing:2.0.0-alpha01") docs("androidx.tracing:tracing-ktx:2.0.0-alpha01") @@ -423,27 +423,27 @@ dependencies { docs("androidx.versionedparcelable:versionedparcelable:1.2.1") docs("androidx.viewpager2:viewpager2:1.1.0") docs("androidx.viewpager:viewpager:1.1.0") - docs("androidx.wear.compose:compose-foundation:1.6.0-alpha09") - docs("androidx.wear.compose:compose-material:1.6.0-alpha09") - docs("androidx.wear.compose:compose-material-core:1.6.0-alpha09") - docs("androidx.wear.compose:compose-material3:1.6.0-alpha09") - docs("androidx.wear.compose:compose-navigation:1.6.0-alpha09") - docs("androidx.wear.compose:compose-navigation3:1.6.0-alpha09") - docs("androidx.wear.compose:compose-ui-tooling:1.6.0-alpha09") - docs("androidx.wear.protolayout:protolayout:1.4.0-alpha05") - docs("androidx.wear.protolayout:protolayout-expression:1.4.0-alpha05") - docs("androidx.wear.protolayout:protolayout-expression-pipeline:1.4.0-alpha05") - docs("androidx.wear.protolayout:protolayout-material:1.4.0-alpha05") - docs("androidx.wear.protolayout:protolayout-material-core:1.4.0-alpha05") - docs("androidx.wear.protolayout:protolayout-material3:1.4.0-alpha05") - docs("androidx.wear.protolayout:protolayout-renderer:1.4.0-alpha05") - docs("androidx.wear.protolayout:protolayout-testing:1.4.0-alpha05") - docs("androidx.wear.tiles:tiles:1.6.0-alpha05") - docs("androidx.wear.tiles:tiles-material:1.6.0-alpha05") - docs("androidx.wear.tiles:tiles-renderer:1.6.0-alpha05") - docs("androidx.wear.tiles:tiles-testing:1.6.0-alpha05") - docs("androidx.wear.tiles:tiles-tooling:1.6.0-alpha05") - docs("androidx.wear.tiles:tiles-tooling-preview:1.6.0-alpha05") + docs("androidx.wear.compose:compose-foundation:1.6.0-alpha10") + docs("androidx.wear.compose:compose-material:1.6.0-alpha10") + docs("androidx.wear.compose:compose-material-core:1.6.0-alpha10") + docs("androidx.wear.compose:compose-material3:1.6.0-alpha10") + docs("androidx.wear.compose:compose-navigation:1.6.0-alpha10") + docs("androidx.wear.compose:compose-navigation3:1.6.0-alpha10") + docs("androidx.wear.compose:compose-ui-tooling:1.6.0-alpha10") + docs("androidx.wear.protolayout:protolayout:1.4.0-beta01") + docs("androidx.wear.protolayout:protolayout-expression:1.4.0-beta01") + docs("androidx.wear.protolayout:protolayout-expression-pipeline:1.4.0-beta01") + docs("androidx.wear.protolayout:protolayout-material:1.4.0-beta01") + docs("androidx.wear.protolayout:protolayout-material-core:1.4.0-beta01") + docs("androidx.wear.protolayout:protolayout-material3:1.4.0-beta01") + docs("androidx.wear.protolayout:protolayout-renderer:1.4.0-beta01") + docs("androidx.wear.protolayout:protolayout-testing:1.4.0-beta01") + docs("androidx.wear.tiles:tiles:1.6.0-beta01") + docs("androidx.wear.tiles:tiles-material:1.6.0-beta01") + docs("androidx.wear.tiles:tiles-renderer:1.6.0-beta01") + docs("androidx.wear.tiles:tiles-testing:1.6.0-beta01") + docs("androidx.wear.tiles:tiles-tooling:1.6.0-beta01") + docs("androidx.wear.tiles:tiles-tooling-preview:1.6.0-beta01") docs("androidx.wear.watchface:watchface:1.3.0-alpha07") docs("androidx.wear.watchface:watchface-client:1.3.0-alpha07") docs("androidx.wear.watchface:watchface-client-guava:1.3.0-alpha07") @@ -467,8 +467,8 @@ dependencies { docs("androidx.wear:wear-phone-interactions:1.1.0") docs("androidx.wear:wear-remote-interactions:1.2.0-rc01") docs("androidx.wear:wear-tooling-preview:1.0.0") - docs("androidx.webgpu:webgpu:1.0.0-alpha03") - docs("androidx.webkit:webkit:1.16.0-alpha01") + docs("androidx.webgpu:webgpu:1.0.0-alpha04") + docs("androidx.webkit:webkit:1.16.0-alpha02") docs("androidx.window.extensions.core:core:1.0.0") docs("androidx.window:window:1.6.0-alpha01") stubs(fileTree(dir: "../window/stubs/", include: ["window-sidecar-release-0.1.0-alpha01.aar"])) @@ -496,7 +496,7 @@ dependencies { docs("androidx.xr.compose.material3:material3:1.0.0-alpha14") docs("androidx.xr.compose:compose:1.0.0-alpha10") docs("androidx.xr.compose:compose-testing:1.0.0-alpha10") - docs("androidx.xr.glimmer:glimmer:1.0.0-alpha05") + docs("androidx.xr.glimmer:glimmer:1.0.0-alpha06") docs("androidx.xr.projected:projected:1.0.0-alpha04") docs("androidx.xr.projected:projected-binding:1.0.0-alpha04") docs("androidx.xr.runtime:runtime:1.0.0-alpha10") From 91e3010e2728f7f236d30da1f21e79db8915afb0 Mon Sep 17 00:00:00 2001 From: Daniel Kim Date: Fri, 6 Feb 2026 11:04:48 -0800 Subject: [PATCH 20/21] Remove obsolete TODO from ProviderImportCredentialsResponse. The TODO comment about replacing callingAppInfo with attestation is no longer applicable. Test: N/A Change-Id: I52fe42abbb89208e32a33cb1a46d3d1bbb5ebc26 --- .../transfer/ProviderImportCredentialsResponse.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/credentials/providerevents/providerevents/src/main/java/androidx/credentials/providerevents/transfer/ProviderImportCredentialsResponse.kt b/credentials/providerevents/providerevents/src/main/java/androidx/credentials/providerevents/transfer/ProviderImportCredentialsResponse.kt index a68b9ca456c5b..9faee0ca90f70 100644 --- a/credentials/providerevents/providerevents/src/main/java/androidx/credentials/providerevents/transfer/ProviderImportCredentialsResponse.kt +++ b/credentials/providerevents/providerevents/src/main/java/androidx/credentials/providerevents/transfer/ProviderImportCredentialsResponse.kt @@ -25,8 +25,6 @@ import androidx.credentials.provider.CallingAppInfo * * @property response a response of credential import * @property callingAppInfo the exporter's app info - * - * TODO(b/445237915): Replace callingAppInfo with attestation */ public class ProviderImportCredentialsResponse( public val response: ImportCredentialsResponse, From 28f29638c96b96703527efced4a045839975bcee Mon Sep 17 00:00:00 2001 From: AndroidX Core Team Date: Fri, 6 Feb 2026 11:05:59 -0800 Subject: [PATCH 21/21] Edited instructions for connecting to studio profiler. PiperOrigin-RevId: 866533837 Change-Id: I3696036087c9dc20d4469b82481250024f5077f6 --- docs/benchmarking.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/benchmarking.md b/docs/benchmarking.md index 5ac3fc204b0df..f1162942e1864 100644 --- a/docs/benchmarking.md +++ b/docs/benchmarking.md @@ -173,14 +173,15 @@ First, set your benchmark to be debuggable in your benchmark module's ```xml ``` Note that switching to the debug variant will likely not work, as Studio will fail to find the benchmark as a test source. -Next select `ConnectedAllocation` in your benchmark module's `build.gradle`: +Next select `ConnectedAllocation` in your benchmark module's `build.gradle` and +suppress AOT warning due to debuggable being "true": ```groovy android { @@ -189,6 +190,7 @@ android { // pause for manual profiler connection before/after a single run of // the benchmark loop, after warmup testInstrumentationRunnerArgument 'androidx.benchmark.profiling.mode', 'ConnectedAllocation' + testInstrumentationRunnerArgument 'androidx.benchmark.suppressErrors', 'NOT-AOT-COMPILED' } } ```