Processar gestos de volta e animações de volta preditiva

Você pode estender a classe abstrata NavigationEventHandler para processar eventos de navegação em várias plataformas. Essa classe oferece métodos correspondentes ao ciclo de vida de um gesto de navegação.

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

A função addHandler conecta o manipulador ao dispatcher:

navigationEventDispatcher.addHandler(myHandler)

Chame myHandler.remove() para remover o manipulador do dispatcher:

myHandler.remove()

Os manipuladores são invocados com base na prioridade e, em seguida, na recência. Todos os gerenciadores de PRIORITY_OVERLAY são chamados antes dos gerenciadores de PRIORITY_DEFAULT. Em cada grupo de prioridade, os manipuladores são invocados em uma ordem LIFO (último a entrar, primeiro a sair). O manipulador adicionado mais recentemente é chamado primeiro.

Interceptar a ação de voltar com o Jetpack Compose

Para o Jetpack Compose, a biblioteca oferece uma função combinável de utilidade para gerenciar a hierarquia de dispatcher.

O elemento combinável NavigationBackHandler cria um NavigationEventHandler para o conteúdo dele e o vincula ao LocalNavigationEventDispatcherOwner. Ele usa o DisposableEffect do Compose para chamar automaticamente o método dispose() do dispatcher quando o elemento combinável sai da tela, gerenciando recursos com segurança.

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

}

Com essa função, você controla o processamento de eventos com precisão em subárvores de interface localizada.

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

Este exemplo mostra como observar atualizações do gesto de volta preditivo usando NavigationEventTransitionState. O valor progress pode ser usado para atualizar elementos da interface em resposta ao gesto de voltar, processando a conclusão e o cancelamento por NavigationBackHandler.

Acessar o gesto de retorno ou deslizar na borda no Compose

Figura 1. Uma animação de volta preditiva criada com NavigationEvent e Compose.

Para animar a tela enquanto o usuário desliza para trás, é necessário (a) verificar se o NavigationEventTransitionState é InProgress e (b) observar o estado de progresso e borda de deslizar com rememberNavigationEventState:

  • progress: um ponto flutuante de 0.0 a 1.0 que indica a distância que o usuário deslizou.
  • swipeEdge: uma constante inteira (EDGE_LEFT ou EDGE_RIGHT) que indica onde o gesto começou.

O snippet a seguir é um exemplo simplificado de como implementar uma animação de escala e mudança:

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