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 que corresponden 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 despachador:

navigationEventDispatcher.addHandler(myHandler)

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

myHandler.remove()

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

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

Para Jetpack Compose, la biblioteca proporciona un elemento componible de utilidad para administrar la jerarquía de despachadores.

El elemento componible NavigationBackHandler crea un NavigationEventHandler para su contenido y lo vincula al LocalNavigationEventDispatcherOwner. Usa DisposableEffect de Compose para llamar automáticamente al método dispose() del despachador cuando la función de componibilidad sale de la pantalla, lo que administra 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 manejo 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 atrás predictivo con NavigationEventTransitionState. El valor progress se puede usar para actualizar los elementos de la IU en respuesta al gesto de retroceso, 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. Una animación del gesto atrás predictivo compilada 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 valor de punto flotante de 0.0 a 1.0 que indica qué tan lejos deslizó el usuario.
  • swipeEdge: Es una constante de número entero (EDGE_LEFT o EDGE_RIGHT) que indica dónde comenzó el gesto.

En el siguiente fragmento, se muestra 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) {
        NavigationEvent.EDGE_LEFT -> (backProgress * maxShift).dp
        NavigationEvent.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
    }
}