Gestire i gesti Indietro e le animazioni predittive per Indietro

Puoi estendere la classe astratta NavigationEventHandler per gestire gli eventi di navigazione su tutte le piattaforme. Questa classe fornisce metodi corrispondenti al ciclo di vita di un gesto di navigazione.

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
    }
}

La funzione addHandler collega il gestore al dispatcher:

navigationEventDispatcher.addHandler(myHandler)

Chiama myHandler.remove() per rimuovere il gestore dal dispatcher:

myHandler.remove()

I gestori vengono richiamati in base alla priorità e poi alla data più recente. Tutti i gestori PRIORITY_OVERLAY vengono chiamati prima di qualsiasi gestore PRIORITY_DEFAULT. All'interno di ogni gruppo di priorità, i gestori vengono richiamati in ordine LIFO (Last-In, First-Out): il gestore aggiunto più di recente viene chiamato per primo.

Intercettare il pulsante Indietro con Jetpack Compose

Per Jetpack Compose, la libreria fornisce un componibile di utilità per gestire la gerarchia dei dispatcher.

Il composable NavigationBackHandler crea un NavigationEventHandler per i suoi contenuti e lo collega a LocalNavigationEventDispatcherOwner. Utilizza DisposableEffect di Compose per chiamare automaticamente il metodo dispose() del dispatcher quando il composable esce dallo schermo, gestendo le risorse in modo sicuro.

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

}

Questa funzione ti consente di controllare con precisione la gestione degli eventi all'interno degli alberi secondari dell'interfaccia utente localizzata.

@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()
        }
    )
}

Questo esempio mostra come osservare gli aggiornamenti del gesto Indietro predittivo utilizzando NavigationEventTransitionState. Il valore progress può essere utilizzato per aggiornare gli elementi dell'interfaccia utente in risposta al gesto Indietro, gestendo il completamento e l'annullamento tramite NavigationBackHandler.

Accedere al gesto Indietro o scorrere il bordo in Scrittura

Figura 1. Un'animazione predittiva per Indietro creata con NavigationEvent e Compose.

Per animare lo schermo mentre l'utente scorre indietro, devi (a) controllare se NavigationEventTransitionState è InProgress e (b) osservare lo stato di avanzamento e il bordo di scorrimento con rememberNavigationEventState:

  • progress: un valore Float da 0.0 a 1.0 che indica la distanza percorsa dall'utente con lo scorrimento.
  • swipeEdge: una costante intera (EDGE_LEFT o EDGE_RIGHT) che indica dove è iniziato il gesto.

Il seguente snippet è un esempio semplificato di come implementare un'animazione di ridimensionamento e spostamento:

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
    }
}