diff --git a/compose/ui/ui-backhandler/src/jbMain/kotlin/androidx/compose/ui/backhandler/BackHandler.jb.kt b/compose/ui/ui-backhandler/src/jbMain/kotlin/androidx/compose/ui/backhandler/BackHandler.jb.kt index 2fe09a7fc4f7e..6418b175c9d88 100644 --- a/compose/ui/ui-backhandler/src/jbMain/kotlin/androidx/compose/ui/backhandler/BackHandler.jb.kt +++ b/compose/ui/ui-backhandler/src/jbMain/kotlin/androidx/compose/ui/backhandler/BackHandler.jb.kt @@ -19,10 +19,25 @@ package androidx.compose.ui.backhandler import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.navigationevent.NavigationEvent +import androidx.navigationevent.NavigationEventInfo +import androidx.navigationevent.NavigationEventTransitionState import androidx.navigationevent.compose.LocalNavigationEventDispatcherOwner +import androidx.navigationevent.compose.NavigationBackHandler +import androidx.navigationevent.compose.rememberNavigationEventState import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.launch @Deprecated("Use NavigationEventHandler instead") @ExperimentalComposeUiApi @@ -31,33 +46,69 @@ actual fun PredictiveBackHandler( enabled: Boolean, onBack: suspend (progress: Flow) -> Unit ) { - LocalNavigationEventDispatcherOwner.current ?: return - /* - TODO: https://youtrack.jetbrains.com/issue/CMP-8937 - NavigationEventHandler(enabled) { progress -> - val compatProgress = progress.map { navEvent -> + val coroutineScope = rememberCoroutineScope() + val navEventState = rememberNavigationEventState(NavigationEventInfo.None) + + var progressChannel: Channel? by remember(onBack) { + mutableStateOf(null) + } + + fun getActiveProgressChannel(): Channel { + val currentProgressChannel = progressChannel + if (currentProgressChannel == null) { + val progress = Channel() + progressChannel = progress + coroutineScope.launch { + onBack(progress.consumeAsFlow()) + } + return progress + } else { + return currentProgressChannel + } + } + + val transitionState = navEventState.transitionState + if (transitionState is NavigationEventTransitionState.InProgress) { + LaunchedEffect(transitionState) { + val navEvent = transitionState.latestEvent val swipeEdge = when (navEvent.swipeEdge) { - NavigationEventSwipeEdge.Left -> BackEventCompat.EDGE_LEFT - NavigationEventSwipeEdge.Right -> BackEventCompat.EDGE_RIGHT - else -> 0 + NavigationEvent.EDGE_RIGHT -> BackEventCompat.EDGE_RIGHT + else -> BackEventCompat.EDGE_LEFT } - BackEventCompat(navEvent.touchX, navEvent.touchY, navEvent.progress, swipeEdge) + val event = BackEventCompat( + navEvent.touchX, navEvent.touchY, navEvent.progress, swipeEdge + ) + getActiveProgressChannel().send(event) + } + } + + NavigationBackHandler( + state = navEventState, + isBackEnabled = enabled, + onBackCancelled = { + getActiveProgressChannel().close(CancellationException("Cancelled")) + progressChannel = null + }, + onBackCompleted = { + getActiveProgressChannel().close() + progressChannel = null + } + ) + DisposableEffect(Unit) { + onDispose { + progressChannel?.close(CancellationException("Disposed")) + progressChannel = null } - onBack(compatProgress) } - */ } @Deprecated("Use NavigationEventHandler instead") @ExperimentalComposeUiApi @Composable actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) { - PredictiveBackHandler(enabled) { progress -> - try { - progress.collect { /*ignore*/ } - onBack() - } catch (e: CancellationException) { - //ignore - } - } + NavigationBackHandler( + state = rememberNavigationEventState(NavigationEventInfo.None), + isBackEnabled = enabled, + onBackCompleted = onBack + ) } diff --git a/compose/ui/ui-test/build.gradle b/compose/ui/ui-test/build.gradle index 621d6654abc2f..906e3e942ee83 100644 --- a/compose/ui/ui-test/build.gradle +++ b/compose/ui/ui-test/build.gradle @@ -151,6 +151,7 @@ if (AndroidXComposePlugin.isMultiplatformEnabled(project)) { dependencies { implementation(libs.atomicFu) implementation(project(":lifecycle:lifecycle-runtime-compose")) + implementation(project(":navigationevent:navigationevent-compose")) } } diff --git a/compose/ui/ui-test/src/skikoMain/kotlin/androidx/compose/ui/test/ComposeUiTest.skiko.kt b/compose/ui/ui-test/src/skikoMain/kotlin/androidx/compose/ui/test/ComposeUiTest.skiko.kt index f482dd5bd2c2a..6511cc08e2a14 100644 --- a/compose/ui/ui-test/src/skikoMain/kotlin/androidx/compose/ui/test/ComposeUiTest.skiko.kt +++ b/compose/ui/ui-test/src/skikoMain/kotlin/androidx/compose/ui/test/ComposeUiTest.skiko.kt @@ -44,6 +44,9 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.navigationevent.NavigationEventDispatcher +import androidx.navigationevent.NavigationEventDispatcherOwner +import androidx.navigationevent.compose.LocalNavigationEventDispatcherOwner import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.cancellation.CancellationException @@ -456,7 +459,10 @@ open class SkikoComposeUiTest @InternalTestApi constructor( } @OptIn(InternalComposeUiApi::class) - internal inner class SkikoTestOwner : TestOwner, LifecycleOwner { + internal inner class SkikoTestOwner : + TestOwner, + LifecycleOwner, + NavigationEventDispatcherOwner { override val mainClock get() = mainClockImpl @@ -474,6 +480,7 @@ open class SkikoComposeUiTest @InternalTestApi constructor( } override val lifecycle = LifecycleRegistry.createUnsafe(this) + override val navigationEventDispatcher = NavigationEventDispatcher() fun captureToImage(semanticsNode: SemanticsNode): ImageBitmap = this@SkikoComposeUiTest.captureToImage(semanticsNode) @@ -535,6 +542,7 @@ open class SkikoComposeUiTest @InternalTestApi constructor( private fun ProvideCommonCompositionLocals(content: @Composable () -> Unit) { CompositionLocalProvider( LocalLifecycleOwner provides testOwner, + LocalNavigationEventDispatcherOwner provides testOwner, content = content, ) } diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/navigationevent/UIKitNavigationEventInput.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/navigationevent/UIKitNavigationEventInput.kt index dc27e06605d68..40969a0e48549 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/navigationevent/UIKitNavigationEventInput.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/navigationevent/UIKitNavigationEventInput.kt @@ -30,7 +30,6 @@ import androidx.compose.ui.unit.asDpRect import androidx.compose.ui.unit.toOffset import androidx.compose.ui.unit.width import androidx.navigationevent.NavigationEvent -import androidx.navigationevent.NavigationEventDispatcher import androidx.navigationevent.NavigationEventInput import kotlin.math.abs import kotlinx.cinterop.BetaInteropApi @@ -57,13 +56,6 @@ internal class UIKitNavigationEventInput( private val density: Density, private val getTopLeftOffsetInWindow: () -> IntOffset ) : NavigationEventInput() { - val isBackGestureActive: Boolean - get() = false - fun onDidMoveToWindow(window: UIWindow?, composeRootView: UIView) {} - fun onKeyEvent(event: KeyEvent): Boolean = false -/* -TODO: https://youtrack.jetbrains.com/issue/CMP-8937 - companion object { private const val BACK_GESTURE_SCREEN_SIZE = 0.3 private const val BACK_GESTURE_VELOCITY = 100 @@ -85,24 +77,9 @@ TODO: https://youtrack.jetbrains.com/issue/CMP-8937 edges = UIRectEdgeRight } - override fun onAdded(dispatcher: NavigationEventDispatcher) { - super.onAdded(dispatcher) - updateRecognizersEnabledState(dispatcher.hasEnabledCallbacks()) - } - - override fun onHasEnabledCallbacksChanged(hasEnabledCallbacks: Boolean) { - super.onHasEnabledCallbacksChanged(hasEnabledCallbacks) - updateRecognizersEnabledState(hasEnabledCallbacks) - } - - override fun onRemoved() { - super.onRemoved() - updateRecognizersEnabledState(false) - } - - private fun updateRecognizersEnabledState(isEnabled: Boolean) { - leftEdgePanGestureRecognizer.enabled = isEnabled - rightEdgePanGestureRecognizer.enabled = isEnabled + override fun onHasEnabledHandlersChanged(hasEnabledHandlers: Boolean) { + leftEdgePanGestureRecognizer.enabled = hasEnabledHandlers + rightEdgePanGestureRecognizer.enabled = hasEnabledHandlers } private val activeGestureStates = listOf( @@ -140,7 +117,7 @@ TODO: https://youtrack.jetbrains.com/issue/CMP-8937 fun onKeyEvent(event: KeyEvent): Boolean { if (event.type == KeyEventType.KeyDown && event.key == Key.Escape) { - dispatchOnCompleted() + dispatchOnBackCompleted() return true } else { return false @@ -157,7 +134,7 @@ TODO: https://youtrack.jetbrains.com/issue/CMP-8937 val touch = recognizer.locationOfTouch(0u, view).asDpOffset() val eventOffset = touch.toOffset(density) - getTopLeftOffsetInWindow().toOffset() - dispatchOnStarted( + dispatchOnBackStarted( NavigationEvent( touchX = eventOffset.x, touchY = eventOffset.y, @@ -183,7 +160,7 @@ TODO: https://youtrack.jetbrains.com/issue/CMP-8937 } else { (bounds.width - touch.x) / bounds.width } - dispatchOnProgressed( + dispatchOnBackProgressed( NavigationEvent( touchX = eventOffset.x, touchY = eventOffset.y, @@ -210,13 +187,13 @@ TODO: https://youtrack.jetbrains.com/issue/CMP-8937 if (edge == UIRectEdgeLeft) this@velocity.x else -this@velocity.x when { //if movement is fast in the right direction - velX > BACK_GESTURE_VELOCITY -> dispatchOnCompleted() + velX > BACK_GESTURE_VELOCITY -> dispatchOnBackCompleted() //if movement is backward - velX < -10 -> dispatchOnCancelled() + velX < -10 -> dispatchOnBackCancelled() //if there is no movement, or the movement is slow, //but the touch is already more than BACK_GESTURE_SCREEN_SIZE - abs(x) >= size.width * BACK_GESTURE_SCREEN_SIZE -> dispatchOnCompleted() - else -> dispatchOnCancelled() + abs(x) >= size.width * BACK_GESTURE_SCREEN_SIZE -> dispatchOnBackCompleted() + else -> dispatchOnBackCancelled() } } } @@ -224,16 +201,15 @@ TODO: https://youtrack.jetbrains.com/issue/CMP-8937 } UIGestureRecognizerStateCancelled -> { - dispatchOnCancelled() + dispatchOnBackCancelled() } UIGestureRecognizerStateFailed -> { - dispatchOnCompleted() + dispatchOnBackCompleted() } } } } -*/ } /** diff --git a/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/window/ComposeWindow.w3c.kt b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/window/ComposeWindow.w3c.kt index 67c138cdce648..969a901dd7101 100644 --- a/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/window/ComposeWindow.w3c.kt +++ b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/window/ComposeWindow.w3c.kt @@ -42,6 +42,7 @@ import androidx.compose.ui.input.pointer.composeButton import androidx.compose.ui.input.pointer.composeButtons import androidx.compose.ui.platform.DefaultInputModeManager import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalInternalNavigationEventDispatcherOwner import androidx.compose.ui.platform.LocalInternalViewModelStoreOwner import androidx.compose.ui.platform.PlatformContext import androidx.compose.ui.platform.PlatformDragAndDropManager @@ -73,6 +74,8 @@ import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.ViewModelStore import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.navigationevent.NavigationEventDispatcher +import androidx.navigationevent.NavigationEventDispatcherOwner import kotlin.coroutines.coroutineContext import kotlin.math.absoluteValue import kotlinx.browser.document @@ -189,7 +192,7 @@ internal class ComposeWindow( private val configuration: ComposeViewportConfiguration, content: @Composable () -> Unit, private val state: ComposeWindowState -) : LifecycleOwner, ViewModelStoreOwner { +) : LifecycleOwner, ViewModelStoreOwner, NavigationEventDispatcherOwner { private var isDisposed = false private val density: Density = Density( @@ -322,6 +325,7 @@ internal class ComposeWindow( override val lifecycle = LifecycleRegistry(this) override val viewModelStore = ViewModelStore() + override val navigationEventDispatcher = NavigationEventDispatcher() private fun addTypedEvent( type: String, @@ -426,6 +430,7 @@ internal class ComposeWindow( LocalSystemTheme provides systemThemeObserver.currentSystemTheme.value, LocalLifecycleOwner provides this, LocalInternalViewModelStoreOwner provides this, + LocalInternalNavigationEventDispatcherOwner provides this, LocalInteropContainer provides interopContainer, LocalActiveClipEventsTarget provides { (platformContext.textInputService as WebTextInputService).getBackingInput() ?: canvas diff --git a/navigation3/navigation3-ui/api/desktop/navigation3-ui.api b/navigation3/navigation3-ui/api/desktop/navigation3-ui.api index 7b86f5915c41e..18d48176a93d7 100644 --- a/navigation3/navigation3-ui/api/desktop/navigation3-ui.api +++ b/navigation3/navigation3-ui/api/desktop/navigation3-ui.api @@ -81,9 +81,6 @@ public final class androidx/navigation3/ui/NavDisplayKt { public static final fun NavDisplay (Landroidx/navigation3/scene/SceneState;Landroidx/navigationevent/compose/NavigationEventState;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Landroidx/compose/animation/SizeTransform;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V public static final fun NavDisplay (Ljava/util/List;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Landroidx/navigation3/scene/SceneStrategy;Landroidx/compose/animation/SizeTransform;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V public static final fun NavDisplay (Ljava/util/List;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Lkotlin/jvm/functions/Function1;Ljava/util/List;Landroidx/navigation3/scene/SceneStrategy;Landroidx/compose/animation/SizeTransform;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;III)V -} - -public final class androidx/navigation3/ui/NavDisplay_nonAndroidKt { public static final fun defaultPopTransitionSpec ()Lkotlin/jvm/functions/Function1; public static final fun defaultPredictivePopTransitionSpec ()Lkotlin/jvm/functions/Function2; public static final fun defaultTransitionSpec ()Lkotlin/jvm/functions/Function1; diff --git a/navigation3/navigation3-ui/src/commonMain/kotlin/androidx/navigation3/ui/NavDisplay.kt b/navigation3/navigation3-ui/src/commonMain/kotlin/androidx/navigation3/ui/NavDisplay.kt index 592e85092eb23..3b46361cefe1c 100644 --- a/navigation3/navigation3-ui/src/commonMain/kotlin/androidx/navigation3/ui/NavDisplay.kt +++ b/navigation3/navigation3-ui/src/commonMain/kotlin/androidx/navigation3/ui/NavDisplay.kt @@ -14,6 +14,9 @@ * limitations under the License. */ +@file:JvmName("NavDisplayKt") +@file:JvmMultifileClass + package androidx.navigation3.ui import androidx.collection.mutableObjectFloatMapOf @@ -59,6 +62,8 @@ import androidx.navigationevent.NavigationEventTransitionState.InProgress import androidx.navigationevent.compose.NavigationBackHandler import androidx.navigationevent.compose.NavigationEventState import androidx.navigationevent.compose.rememberNavigationEventState +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName import kotlin.reflect.KClass import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch diff --git a/navigation3/navigation3-ui/src/desktopMain/kotlin/androidx/navigation3/ui/NavDisplay.desktop.kt b/navigation3/navigation3-ui/src/desktopMain/kotlin/androidx/navigation3/ui/NavDisplay.desktop.kt new file mode 100644 index 0000000000000..bf9ed1842583b --- /dev/null +++ b/navigation3/navigation3-ui/src/desktopMain/kotlin/androidx/navigation3/ui/NavDisplay.desktop.kt @@ -0,0 +1,54 @@ +/* + * 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. + */ + +@file:JvmName("NavDisplayKt") +@file:JvmMultifileClass + +package androidx.navigation3.ui + +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.ContentTransform +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.navigation3.scene.Scene +import androidx.navigationevent.NavigationEvent.SwipeEdge + +private const val DEFAULT_TRANSITION_DURATION_MILLISECOND = 700 + +public actual fun defaultTransitionSpec(): + AnimatedContentTransitionScope>.() -> ContentTransform = { + ContentTransform( + fadeIn(animationSpec = tween(DEFAULT_TRANSITION_DURATION_MILLISECOND)), + fadeOut(animationSpec = tween(DEFAULT_TRANSITION_DURATION_MILLISECOND)), + ) +} + +public actual fun defaultPopTransitionSpec(): + AnimatedContentTransitionScope>.() -> ContentTransform = { + ContentTransform( + fadeIn(animationSpec = tween(DEFAULT_TRANSITION_DURATION_MILLISECOND)), + fadeOut(animationSpec = tween(DEFAULT_TRANSITION_DURATION_MILLISECOND)), + ) +} + +public actual fun defaultPredictivePopTransitionSpec(): + AnimatedContentTransitionScope>.(@SwipeEdge Int) -> ContentTransform = { + ContentTransform( + fadeIn(animationSpec = tween(DEFAULT_TRANSITION_DURATION_MILLISECOND)), + fadeOut(animationSpec = tween(DEFAULT_TRANSITION_DURATION_MILLISECOND)), + ) +} diff --git a/navigation3/navigation3-ui/src/nonAndroidMain/kotlin/androidx/navigation3/ui/NavDisplay.nonAndroid.kt b/navigation3/navigation3-ui/src/macosMain/kotlin/androidx/navigation3/ui/NavDisplay.macos.kt similarity index 54% rename from navigation3/navigation3-ui/src/nonAndroidMain/kotlin/androidx/navigation3/ui/NavDisplay.nonAndroid.kt rename to navigation3/navigation3-ui/src/macosMain/kotlin/androidx/navigation3/ui/NavDisplay.macos.kt index 3919117bfd1f3..e0fbb65e9a124 100644 --- a/navigation3/navigation3-ui/src/nonAndroidMain/kotlin/androidx/navigation3/ui/NavDisplay.nonAndroid.kt +++ b/navigation3/navigation3-ui/src/macosMain/kotlin/androidx/navigation3/ui/NavDisplay.macos.kt @@ -18,15 +18,34 @@ package androidx.navigation3.ui import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.ContentTransform +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.navigation3.scene.Scene -import androidx.navigationevent.NavigationEvent +import androidx.navigationevent.NavigationEvent.SwipeEdge + +private const val DEFAULT_TRANSITION_DURATION_MILLISECOND = 700 public actual fun defaultTransitionSpec(): - AnimatedContentTransitionScope>.() -> ContentTransform = implementedInJetBrainsFork() + AnimatedContentTransitionScope>.() -> ContentTransform = { + ContentTransform( + fadeIn(animationSpec = tween(DEFAULT_TRANSITION_DURATION_MILLISECOND)), + fadeOut(animationSpec = tween(DEFAULT_TRANSITION_DURATION_MILLISECOND)), + ) +} public actual fun defaultPopTransitionSpec(): - AnimatedContentTransitionScope>.() -> ContentTransform = implementedInJetBrainsFork() + AnimatedContentTransitionScope>.() -> ContentTransform = { + ContentTransform( + fadeIn(animationSpec = tween(DEFAULT_TRANSITION_DURATION_MILLISECOND)), + fadeOut(animationSpec = tween(DEFAULT_TRANSITION_DURATION_MILLISECOND)), + ) +} public actual fun defaultPredictivePopTransitionSpec(): - AnimatedContentTransitionScope>.(@NavigationEvent.SwipeEdge Int) -> ContentTransform = - implementedInJetBrainsFork() + AnimatedContentTransitionScope>.(@SwipeEdge Int) -> ContentTransform = { + ContentTransform( + fadeIn(animationSpec = tween(DEFAULT_TRANSITION_DURATION_MILLISECOND)), + fadeOut(animationSpec = tween(DEFAULT_TRANSITION_DURATION_MILLISECOND)), + ) +} diff --git a/navigation3/navigation3-ui/src/nonAndroidMain/kotlin/androidx/navigation3/ui/NotImplemented.nonAndroid.kt b/navigation3/navigation3-ui/src/nonAndroidMain/kotlin/androidx/navigation3/ui/NotImplemented.nonAndroid.kt deleted file mode 100644 index 02e4ff321d9bc..0000000000000 --- a/navigation3/navigation3-ui/src/nonAndroidMain/kotlin/androidx/navigation3/ui/NotImplemented.nonAndroid.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.navigation3.ui - -@Suppress("NOTHING_TO_INLINE") -internal inline fun implementedInJetBrainsFork(): Nothing = - throw NotImplementedError( - """ - Implemented only in JetBrains fork. - """ - .trimIndent() - ) diff --git a/navigation3/navigation3-ui/src/uikitMain/kotlin/androidx/navigation3/ui/NavDisplay.uikit.kt b/navigation3/navigation3-ui/src/uikitMain/kotlin/androidx/navigation3/ui/NavDisplay.uikit.kt new file mode 100644 index 0000000000000..f91a66b61c9a7 --- /dev/null +++ b/navigation3/navigation3-ui/src/uikitMain/kotlin/androidx/navigation3/ui/NavDisplay.uikit.kt @@ -0,0 +1,83 @@ +/* + * 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.navigation3.ui + +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.ContentTransform +import androidx.compose.animation.core.CubicBezierEasing +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleOut +import androidx.navigation3.scene.Scene +import androidx.navigationevent.NavigationEvent.Companion.EDGE_LEFT +import androidx.navigationevent.NavigationEvent.SwipeEdge + +private const val DEFAULT_TRANSITION_DURATION_MILLISECOND = 500 +private val IosTransitionEasing = CubicBezierEasing(0.2833f, 0.99f, 0.31833f, 0.99f) + +public actual fun defaultTransitionSpec(): + AnimatedContentTransitionScope>.() -> ContentTransform = { + ContentTransform( + slideIntoContainer( + towards = AnimatedContentTransitionScope.SlideDirection.Left, + animationSpec = tween(DEFAULT_TRANSITION_DURATION_MILLISECOND, easing = IosTransitionEasing), + ), + slideOutOfContainer( + towards = AnimatedContentTransitionScope.SlideDirection.Left, + targetOffset = { it / 4 }, + animationSpec = tween(DEFAULT_TRANSITION_DURATION_MILLISECOND, easing = IosTransitionEasing), + ), + ) +} + +public actual fun defaultPopTransitionSpec(): + AnimatedContentTransitionScope>.() -> ContentTransform = { + ContentTransform( + slideIntoContainer( + towards = AnimatedContentTransitionScope.SlideDirection.Right, + initialOffset = { it / 4 }, + animationSpec = tween(DEFAULT_TRANSITION_DURATION_MILLISECOND, easing = IosTransitionEasing), + ), + slideOutOfContainer( + towards = AnimatedContentTransitionScope.SlideDirection.Right, + animationSpec = tween(DEFAULT_TRANSITION_DURATION_MILLISECOND, easing = IosTransitionEasing), + ), + ) +} + +public actual fun defaultPredictivePopTransitionSpec(): + AnimatedContentTransitionScope>.(@SwipeEdge Int) -> ContentTransform = { edge -> + val towards = if (edge == EDGE_LEFT) { + AnimatedContentTransitionScope.SlideDirection.Right + } else { + AnimatedContentTransitionScope.SlideDirection.Left + } + ContentTransform( + slideIntoContainer( + towards = towards, + initialOffset = { it / 4 }, + animationSpec = tween(DEFAULT_TRANSITION_DURATION_MILLISECOND, easing = LinearEasing), + ), + slideOutOfContainer( + towards = towards, + animationSpec = tween(DEFAULT_TRANSITION_DURATION_MILLISECOND, easing = LinearEasing), + ), + ) +} diff --git a/navigation3/navigation3-ui/src/webMain/kotlin/androidx/navigation3/ui/NavDisplay.web.kt b/navigation3/navigation3-ui/src/webMain/kotlin/androidx/navigation3/ui/NavDisplay.web.kt new file mode 100644 index 0000000000000..e0fbb65e9a124 --- /dev/null +++ b/navigation3/navigation3-ui/src/webMain/kotlin/androidx/navigation3/ui/NavDisplay.web.kt @@ -0,0 +1,51 @@ +/* + * 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.navigation3.ui + +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.ContentTransform +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.navigation3.scene.Scene +import androidx.navigationevent.NavigationEvent.SwipeEdge + +private const val DEFAULT_TRANSITION_DURATION_MILLISECOND = 700 + +public actual fun defaultTransitionSpec(): + AnimatedContentTransitionScope>.() -> ContentTransform = { + ContentTransform( + fadeIn(animationSpec = tween(DEFAULT_TRANSITION_DURATION_MILLISECOND)), + fadeOut(animationSpec = tween(DEFAULT_TRANSITION_DURATION_MILLISECOND)), + ) +} + +public actual fun defaultPopTransitionSpec(): + AnimatedContentTransitionScope>.() -> ContentTransform = { + ContentTransform( + fadeIn(animationSpec = tween(DEFAULT_TRANSITION_DURATION_MILLISECOND)), + fadeOut(animationSpec = tween(DEFAULT_TRANSITION_DURATION_MILLISECOND)), + ) +} + +public actual fun defaultPredictivePopTransitionSpec(): + AnimatedContentTransitionScope>.(@SwipeEdge Int) -> ContentTransform = { + ContentTransform( + fadeIn(animationSpec = tween(DEFAULT_TRANSITION_DURATION_MILLISECOND)), + fadeOut(animationSpec = tween(DEFAULT_TRANSITION_DURATION_MILLISECOND)), + ) +}