Как обрабатывать жесты "Назад" и анимацию с предпросмотром для жеста "Назад"

Вы можете расширить абстрактный класс NavigationEventHandler, чтобы обрабатывать события навигации на разных платформах. В этом классе есть методы, соответствующие жизненному циклу жеста навигации.

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

Функция addHandler связывает обработчик с диспетчером:

navigationEventDispatcher.addHandler(myHandler)

Чтобы удалить обработчик из диспетчера, вызовите метод myHandler.remove().

myHandler.remove()

Обработчики вызываются в порядке приоритета, а затем по давности. Все обработчики PRIORITY_OVERLAY вызываются до любых обработчиков PRIORITY_DEFAULT. В каждой группе приоритета обработчики вызываются в порядке "последним добавлен – первым вызван" (LIFO).

Перехват кнопки "Назад" с помощью Jetpack Compose

Для Jetpack Compose библиотека предоставляет компонуемую функцию для управления иерархией диспетчеров.

Composable-функция NavigationBackHandler создает элемент NavigationEventHandler для своего контента и связывает его с элементом LocalNavigationEventDispatcherOwner. В нем используется DisposableEffect из Compose, чтобы автоматически вызывать метод dispose() диспетчера, когда компонент покидает экран, безопасно управляя ресурсами.

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

}

Эта функция позволяет точно управлять обработкой событий в локализованных поддеревьях интерфейса.

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

В этом примере показано, как отслеживать обновления прогнозирующего жеста "Назад" с помощью NavigationEventTransitionState. Значение progress можно использовать для обновления элементов интерфейса в ответ на жест "Назад", а завершение и отмену можно обрабатывать с помощью NavigationBackHandler.

Как использовать жест "Назад" или провести пальцем по краю экрана в режиме создания письма

Рисунок 1. Анимация предпросмотра для жеста "Назад", созданная с помощью NavigationEvent и Compose.

Чтобы анимировать экран, когда пользователь проводит пальцем назад, вам нужно: а) проверить, является ли NavigationEventTransitionState InProgress; б) отслеживать прогресс и состояние края экрана с помощью rememberNavigationEventState:

  • progress: число с плавающей запятой от 0.0 до 1.0, указывающее, насколько далеко пользователь провел пальцем.
  • swipeEdge – целочисленная константа (EDGE_LEFT или EDGE_RIGHT), указывающая, где началось действие.

В приведенном ниже фрагменте кода показано, как реализовать анимацию масштабирования и сдвига:

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