Gérer les gestes Retour et les animations de prévisualisation du Retour

Vous pouvez étendre la classe abstraite NavigationEventHandler pour gérer les événements de navigation sur les plates-formes. Cette classe fournit des méthodes correspondant au cycle de vie d'un geste de navigation.

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 fonction addHandler connecte le gestionnaire au répartiteur :

navigationEventDispatcher.addHandler(myHandler)

Appelez myHandler.remove() pour supprimer le gestionnaire du répartiteur :

myHandler.remove()

Les gestionnaires sont appelés en fonction de leur priorité, puis de leur ancienneté. Tous les gestionnaires PRIORITY_OVERLAY sont appelés avant les gestionnaires PRIORITY_DEFAULT. Dans chaque groupe de priorité, les gestionnaires sont appelés dans l'ordre LIFO (dernier entré, premier sorti) : le gestionnaire ajouté le plus récemment est appelé en premier.

Intercepter le retour avec Jetpack Compose

Pour Jetpack Compose, la bibliothèque fournit un composable utilitaire permettant de gérer la hiérarchie des répartiteurs.

Le composable NavigationBackHandler crée un NavigationEventHandler pour son contenu et l'associe au LocalNavigationEventDispatcherOwner. Il utilise DisposableEffect de Compose pour appeler automatiquement la méthode dispose() du répartiteur lorsque le composable quitte l'écran, ce qui permet de gérer les ressources de manière sécurisée.

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

}

Cette fonction vous permet de contrôler précisément la gestion des événements dans les sous-arborescences d'UI localisées.

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

Cet exemple montre comment observer les mises à jour des gestes de retour prédictifs à l'aide de NavigationEventTransitionState. La valeur progress peut être utilisée pour mettre à jour les éléments d'interface utilisateur en réponse au geste Retour, tout en gérant l'achèvement et l'annulation via NavigationBackHandler.

Accéder au geste de retour ou au balayage sur la bordure dans Compose

Figure 1 : Animation de la prévisualisation du Retour créée avec NavigationEvent et Compose.

Pour animer l'écran pendant que l'utilisateur effectue un balayage vers l'arrière, vous devez (a) vérifier si NavigationEventTransitionState est InProgress et (b) observer l'état de progression et de bordure de balayage avec rememberNavigationEventState :

  • progress : valeur float comprise entre 0.0 et 1.0 indiquant la distance parcourue par l'utilisateur lors du balayage.
  • swipeEdge : constante entière (EDGE_LEFT ou EDGE_RIGHT) indiquant où le geste a commencé.

L'extrait suivant est un exemple simplifié d'implémentation d'une animation de mise à l'échelle et de décalage :

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