Cómo controlar los gestos atrás y las animaciones de atrás predictivo

Puedes extender la clase abstracta NavigationEventHandler para controlar los eventos de navegación en todas las plataformas. Esta clase proporciona métodos correspondientes al ciclo de vida de un gesto de navegación.

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 función addHandler conecta el controlador al distribuidor:

navigationEventDispatcher.addHandler(myHandler)

Llama a myHandler.remove() para quitar el controlador del dispatcher:

myHandler.remove()

Los controladores se invocan según la prioridad y, luego, según la antigüedad. Se llama a todos los controladores de PRIORITY_OVERLAY antes que a los controladores de PRIORITY_DEFAULT. Dentro de cada grupo de prioridad, los controladores se invocan en orden LIFO (último en entrar, primero en salir): el controlador agregado más recientemente se llama primero.

Cómo interceptar el botón Atrás con Jetpack Compose

En el caso de Jetpack Compose, la biblioteca proporciona un elemento componible de utilidad para administrar la jerarquía del dispatcher.

El elemento NavigationBackHandler componible crea un NavigationEventHandler para su contenido y lo vincula al LocalNavigationEventDispatcherOwner. Usa DisposableEffect de Compose para llamar automáticamente al método dispose() del dispatcher cuando el elemento componible abandona la pantalla, lo que permite administrar los recursos de forma segura.

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

}

Esta función te permite controlar el procesamiento de eventos con precisión dentro de subárboles de IU localizados.

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

En este ejemplo, se muestra cómo observar las actualizaciones del gesto de atrás predictivo con NavigationEventTransitionState. El valor de progress se puede usar para actualizar los elementos de la IU en respuesta al gesto de atrás, mientras se controla la finalización y la cancelación a través de NavigationBackHandler.

Cómo acceder al gesto de atrás o al borde de deslizamiento en Compose

Figura 1. Animación del gesto atrás predictivo creada con NavigationEvent y Compose.

Para animar la pantalla mientras el usuario desliza el dedo hacia atrás, deberás (a) verificar si NavigationEventTransitionState es InProgress y (b) observar el progreso y el estado del borde de deslizamiento con rememberNavigationEventState:

  • progress: Es un número de punto flotante de 0.0 a 1.0 que indica qué tanto deslizó el usuario.
  • swipeEdge: Es una constante entera (EDGE_LEFT o EDGE_RIGHT) que indica dónde comenzó el gesto.

El siguiente fragmento es un ejemplo simplificado de cómo implementar una animación de escala y desplazamiento:

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