Obsługa gestów cofania i animacji przewidywanego przejścia wstecz

Możesz rozszerzyć klasę abstrakcyjną NavigationEventHandler, aby obsługiwać zdarzenia nawigacji na różnych platformach. Ta klasa udostępnia metody odpowiadające cyklowi życia gestu nawigacji.

val myHandler = object: NavigationEventHandler<NavigationEventInfo>(
    initialInfo = NavigationEventInfo.None,
    isBackEnabled = true
) {
    override fun onBackStarted(event: NavigationEvent) {
        // Prepare for the back event
    }

    override fun onBackProgressed(event: NavigationEvent) {
        // Use event.progress for predictive animations
    }

    // This is the required method for final event handling
    override fun onBackCompleted() {
        // Complete the back event
    }

    override fun onBackCancelled() {
        // Cancel the back event
    }
}

Funkcja addHandler łączy moduł obsługi z dyspozytorem:

navigationEventDispatcher.addHandler(myHandler)

Zadzwoń pod numer myHandler.remove(), aby usunąć obsługę z dyspozytora:

myHandler.remove()

Moduły obsługi są wywoływane na podstawie priorytetu, a potem na podstawie daty ostatniego użycia. Wszystkie moduły obsługi PRIORITY_OVERLAY są wywoływane przed modułami obsługi PRIORITY_DEFAULT. W każdej grupie priorytetowej moduły obsługi są wywoływane w kolejności LIFO (Last-In, First-Out) – najpierw wywoływany jest moduł obsługi dodany jako ostatni.

Przechwytywanie wstecz za pomocą Jetpack Compose

W przypadku Jetpack Compose biblioteka udostępnia funkcję kompozycyjną narzędzia do zarządzania hierarchią dyspozytorów.

Funkcja kompozycyjna NavigationBackHandler tworzy NavigationEventHandler dla swoich treści i łączy go z LocalNavigationEventDispatcherOwner. Używa funkcji DisposableEffect biblioteki Compose, aby automatycznie wywoływać metodę dispose() mechanizmu dispatcher, gdy funkcja kompozycyjna znika z ekranu, bezpiecznie zarządzając zasobami.

@Composable
public fun NavigationBackHandler(
    state: NavigationEventState<out NavigationEventInfo>,
    isBackEnabled: Boolean = true,
    onBackCancelled: () -> Unit = {},
    onBackCompleted: () -> Unit,
){

}

Ta funkcja umożliwia precyzyjne sterowanie obsługą zdarzeń w poddrzewach zlokalizowanego interfejsu.

@Composable
fun HandlingBackWithTransitionState(
    onNavigateUp: () -> Unit
) {
    val navigationState = rememberNavigationEventState(
        currentInfo = NavigationEventInfo.None
    )
    val transitionState = navigationState.transitionState
    // React to predictive back transition updates
    when (transitionState) {
        is NavigationEventTransitionState.InProgress -> {
            val progress = transitionState.latestEvent.progress
            // Use progress (0f..1f) to update UI during the gesture
        }
        is NavigationEventTransitionState.Idle -> {
            // Reset any temporary UI state if the gesture is cancelled
        }
    }
    NavigationBackHandler(
        state = navigationState,
        onBackCancelled = {
            // Called if the back gesture is cancelled
        },
        onBackCompleted = {
            // Called when the back gesture fully completes
            onNavigateUp()
        }
    )
}

Ten przykład pokazuje, jak obserwować aktualizacje gestu przewidywanego przejścia wstecz za pomocą funkcji NavigationEventTransitionState. Wartość progress może służyć do aktualizowania elementów interfejsu w odpowiedzi na gest powrotu, a obsługa zakończenia i anulowania odbywa się za pomocą NavigationBackHandler.

Dostęp do gestu cofania lub przesunięcia od krawędzi w Compose

Rysunek 1. Animacja przewidywanego przejścia wstecz utworzona za pomocą NavigationEvent i Compose.

Aby animować ekran podczas przesuwania wstecz, musisz (a) sprawdzić, czy NavigationEventTransitionState ma wartość InProgress, oraz (b) obserwować postęp i stan krawędzi przesunięcia za pomocą rememberNavigationEventState:

  • progress: liczba zmiennoprzecinkowa z zakresu od 0.0 do 1.0 wskazująca, jak daleko użytkownik przesunął palcem.
  • swipeEdge: stała liczba całkowita (EDGE_LEFT lub EDGE_RIGHT) wskazująca, gdzie rozpoczął się gest.

Poniższy fragment kodu to uproszczony przykład implementacji animacji skalowania i przesuwania:

object Routes {
    const val SCREEN_A = "Screen A"
    const val SCREEN_B = "Screen B"
}

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            var state by remember { mutableStateOf(Routes.SCREEN_A) }
            val backEventState = rememberNavigationEventState<NavigationEventInfo>(currentInfo = NavigationEventInfo.None)
            when (state) {
                Routes.SCREEN_A -> {
                    ScreenA(onNavigate = { state = Routes.SCREEN_B })
                }
                else -> {
                    if (backEventState.transitionState is NavigationEventTransitionState.InProgress) {
                        ScreenA(onNavigate = { })
                    }
                    ScreenB(
                        backEventState = backEventState,
                        onBackCompleted = { state = Routes.SCREEN_A }
                    )
                }
            }
        }
    }
}

@Composable
fun ScreenB(
    backEventState: NavigationEventState<NavigationEventInfo>,
    onBackCompleted: () -> Unit = {},
) {
    val transitionState = backEventState.transitionState
    val latestEvent =
        (transitionState as? NavigationEventTransitionState.InProgress)
            ?.latestEvent
    val backProgress = latestEvent?.progress ?: 0f
    val swipeEdge = latestEvent?.swipeEdge ?: NavigationEvent.EDGE_LEFT
    if (transitionState is NavigationEventTransitionState.InProgress) {
        Log.d("BackGesture", "Progress: ${transitionState.latestEvent.progress}")
    } else if (transitionState is NavigationEventTransitionState.Idle) {
        Log.d("BackGesture", "Idle")
    }
    val animatedScale by animateFloatAsState(
        targetValue = 1f - (backProgress * 0.1f),
        label = "ScaleAnimation"
    )
    val windowInfo = LocalWindowInfo.current
    val density = LocalDensity.current
    val maxShift = remember(windowInfo, density) {
        val widthDp = with(density) { windowInfo.containerSize.width.toDp() }
        (widthDp.value / 20f) - 8
    }
    val offsetX = when (swipeEdge) {
        EDGE_LEFT -> (backProgress * maxShift).dp
        EDGE_RIGHT -> (-backProgress * maxShift).dp
        else -> 0.dp
    }
    NavigationBackHandler(
        state = backEventState,
        onBackCompleted = onBackCompleted,
        isBackEnabled = true
    )
    Box(
        modifier = Modifier
            .offset(x = offsetX)
            .scale(animatedScale)
    ){
        // Rest of UI
    }
}