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 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) + } + } } } } diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt index ca7caad8fe275..f1236bea13cd5 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 d8445c35b0d09..eb973d221e901 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 } 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/runtime/runtime/src/linuxMain/kotlin/androidx/compose/runtime/platform/Synchronization.linux.kt b/compose/material/material-navigation/src/androidMain/kotlin/androidx/compose/material/navigation/internal/BackHandler.android.kt similarity index 64% rename from compose/runtime/runtime/src/linuxMain/kotlin/androidx/compose/runtime/platform/Synchronization.linux.kt rename to compose/material/material-navigation/src/androidMain/kotlin/androidx/compose/material/navigation/internal/BackHandler.android.kt index 6cf84f07b3ba9..142ff7bf4280a 100644 --- a/compose/runtime/runtime/src/linuxMain/kotlin/androidx/compose/runtime/platform/Synchronization.linux.kt +++ b/compose/material/material-navigation/src/androidMain/kotlin/androidx/compose/material/navigation/internal/BackHandler.android.kt @@ -1,5 +1,5 @@ /* - * Copyright 2026 The Android Open Source Project + * 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. @@ -14,6 +14,12 @@ * limitations under the License. */ -package androidx.compose.runtime.platform +package androidx.compose.material.navigation.internal -internal actual val PTHREAD_MUTEX_RECURSIVE: Int = platform.posix.PTHREAD_MUTEX_RECURSIVE.toInt() +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/runtime/runtime/src/appleMain/kotlin/androidx/compose/runtime/platform/Synchronization.apple.kt b/compose/material/material-navigation/src/commonMain/kotlin/androidx/compose/material/navigation/internal/BackHandler.kt similarity index 70% rename from compose/runtime/runtime/src/appleMain/kotlin/androidx/compose/runtime/platform/Synchronization.apple.kt rename to compose/material/material-navigation/src/commonMain/kotlin/androidx/compose/material/navigation/internal/BackHandler.kt index 7f7ad03813558..5e6490e2320d0 100644 --- a/compose/runtime/runtime/src/appleMain/kotlin/androidx/compose/runtime/platform/Synchronization.apple.kt +++ b/compose/material/material-navigation/src/commonMain/kotlin/androidx/compose/material/navigation/internal/BackHandler.kt @@ -1,5 +1,5 @@ /* - * Copyright 2026 The Android Open Source Project + * 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. @@ -14,6 +14,8 @@ * limitations under the License. */ -package androidx.compose.runtime.platform +package androidx.compose.material.navigation.internal -internal actual val PTHREAD_MUTEX_RECURSIVE: Int = platform.posix.PTHREAD_MUTEX_RECURSIVE +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/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt index 80ba259af723b..95763d4a8cb5b 100644 --- a/compose/material3/material3/api/current.txt +++ b/compose/material3/material3/api/current.txt @@ -3042,6 +3042,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(); @@ -3064,6 +3065,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; @@ -3083,8 +3085,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); @@ -3101,6 +3103,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 { @@ -4605,6 +4609,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 +4638,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..95763d4a8cb5b 100644 --- a/compose/material3/material3/api/restricted_current.txt +++ b/compose/material3/material3/api/restricted_current.txt @@ -3042,6 +3042,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(); @@ -3064,6 +3065,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; @@ -3083,8 +3085,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); @@ -3101,6 +3103,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 { @@ -4605,6 +4609,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 +4638,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/bcv/native/current.txt b/compose/material3/material3/bcv/native/current.txt index acc9c92d068f7..f3d6053f5d3e1 100644 --- a/compose/material3/material3/bcv/native/current.txt +++ b/compose/material3/material3/bcv/native/current.txt @@ -3914,7 +3914,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/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/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/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/androidDeviceTest/kotlin/androidx/compose/material3/ButtonTest.kt b/compose/material3/material3/src/androidDeviceTest/kotlin/androidx/compose/material3/ButtonTest.kt index 07dd257fbf3aa..83af143a4db02 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 @@ -268,10 +268,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! */ }, @@ -298,10 +296,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/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/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 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 } 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, + ) 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/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 } 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/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() diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt index 22e1d4143937e..b0bad87fe3d6c 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; } @@ -157,7 +159,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; @@ -170,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 cf2f0b07f6721..ea06c3773de3d 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; } @@ -157,7 +159,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; @@ -170,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/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 c4b47e974c439..f2dcaebf7803d 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. 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 e50e2ecda9490..5217b17fdc563 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 @@ -120,15 +120,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) 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, diff --git a/docs-public/build.gradle b/docs-public/build.gradle index 62458435ce06a..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") @@ -253,39 +253,41 @@ 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") - docs("androidx.mediarouter:mediarouter:1.8.1") - docs("androidx.mediarouter:mediarouter-testing:1.8.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.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") @@ -299,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") @@ -368,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") @@ -383,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") @@ -403,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") @@ -421,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") @@ -465,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"])) @@ -494,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") diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle index ce1011052b212..2c18d7f7c22d6 100644 --- a/docs-tip-of-tree/build.gradle +++ b/docs-tip-of-tree/build.gradle @@ -83,7 +83,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")) 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' } } ``` 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 }, + ) + } +} 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 bbb435c782c08..0000000000000 Binary files a/ink/ink-brush/src/androidMain/res/drawable-nodpi/pencil_background_v1.png and /dev/null differ diff --git a/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/Brush.kt b/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/Brush.kt index b1db08097a15b..8255253217d81 100644 --- a/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/Brush.kt +++ b/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/Brush.kt @@ -484,26 +484,6 @@ private object BrushNative { @UsedByNative external fun computeComposeColorLong(nativePointer: Long): Long - /** This is a callback used by computeComposeColorLong. */ - @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() - @UsedByNative external fun getSize(nativePointer: Long): Float @UsedByNative external fun getEpsilon(nativePointer: Long): Float diff --git a/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/BrushBehavior.kt b/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/BrushBehavior.kt index a2a2ef44dfab2..588385cb571d9 100644 --- a/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/BrushBehavior.kt +++ b/ink/ink-brush/src/jvmAndAndroidMain/kotlin/androidx/ink/brush/BrushBehavior.kt @@ -59,7 +59,7 @@ import kotlin.jvm.JvmStatic * before being applied: The rates of change of shape properties may be constrained to keep them * from changing too rapidly with respect to distance traveled from one input to the next. */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // FutureJetpackApi @ExperimentalInkCustomBrushApi // NotCloseable: Finalize is only used to free the native peer. @Suppress("NotCloseable") @@ -72,11 +72,19 @@ private constructor( ) { public val terminalNodes: List = 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, 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/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 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) 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 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") + } +}