Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -31,33 +46,69 @@ actual fun PredictiveBackHandler(
enabled: Boolean,
onBack: suspend (progress: Flow<BackEventCompat>) -> Unit

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that onBack is still called after cancellation. Not sure if it's our fault. For example:

PredictiveBackHandler(true) { progress ->
    Log.d("gyz", "start")
    try {
        progress.collect { Log.d("gyz", it.toString()) }
        Log.d("gyz", "finish")
    } catch(e: Exception) {
        Log.d("gyz", "cancelled")
    }
}

And after it prints "cancelled", it then prints another "start" and back event.

) {
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<BackEventCompat>? by remember(onBack) {
mutableStateOf(null)
}

fun getActiveProgressChannel(): Channel<BackEventCompat> {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a TIL question. What is the benefit of using Channel instead of Flow these days? Is it for performance?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a lighter low-level abstraction.

val currentProgressChannel = progressChannel
if (currentProgressChannel == null) {
val progress = Channel<BackEventCompat>()
progressChannel = progress
coroutineScope.launch {
onBack(progress.consumeAsFlow())
}
return progress
} else {
return currentProgressChannel
}
}

val transitionState = navEventState.transitionState
if (transitionState is NavigationEventTransitionState.InProgress) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it get to run when the transition state turns from Idle to InProgress again after a composition?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, nvm, it does.

LaunchedEffect(transitionState) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the coroutine relaunch after every transitionState change? For example, if we see:

1. Idle
2. InProgress(eventA)
3. InProgress(eventB)
4. InProgress(eventC)
5. Idle

Does it launch 5 times?

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
)
}
1 change: 1 addition & 0 deletions compose/ui/ui-test/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ if (AndroidXComposePlugin.isMultiplatformEnabled(project)) {
dependencies {
implementation(libs.atomicFu)
implementation(project(":lifecycle:lifecycle-runtime-compose"))
implementation(project(":navigationevent:navigationevent-compose"))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -474,6 +480,7 @@ open class SkikoComposeUiTest @InternalTestApi constructor(
}

override val lifecycle = LifecycleRegistry.createUnsafe(this)
override val navigationEventDispatcher = NavigationEventDispatcher()

fun captureToImage(semanticsNode: SemanticsNode): ImageBitmap =
[email protected](semanticsNode)
Expand Down Expand Up @@ -535,6 +542,7 @@ open class SkikoComposeUiTest @InternalTestApi constructor(
private fun ProvideCommonCompositionLocals(content: @Composable () -> Unit) {
CompositionLocalProvider(
LocalLifecycleOwner provides testOwner,
LocalNavigationEventDispatcherOwner provides testOwner,
content = content,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -210,30 +187,29 @@ TODO: https://youtrack.jetbrains.com/issue/CMP-8937
if (edge == UIRectEdgeLeft) [email protected] else [email protected]
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()
}
}
}
}
}

UIGestureRecognizerStateCancelled -> {
dispatchOnCancelled()
dispatchOnBackCancelled()
}

UIGestureRecognizerStateFailed -> {
dispatchOnCompleted()
dispatchOnBackCompleted()
}
}
}
}
*/
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -322,6 +325,7 @@ internal class ComposeWindow(

override val lifecycle = LifecycleRegistry(this)
override val viewModelStore = ViewModelStore()
override val navigationEventDispatcher = NavigationEventDispatcher()

private fun <T : Event> addTypedEvent(
type: String,
Expand Down Expand Up @@ -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
Expand Down
3 changes: 0 additions & 3 deletions navigation3/navigation3-ui/api/desktop/navigation3-ui.api
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
* limitations under the License.
*/

@file:JvmName("NavDisplayKt")
@file:JvmMultifileClass

package androidx.navigation3.ui

import androidx.collection.mutableObjectFloatMapOf
Expand Down Expand Up @@ -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
Expand Down
Loading