뒤로 동작 및 뒤로 탐색 예측 애니메이션 처리

추상 클래스 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의 경우 라이브러리는 디스패처 계층 구조를 관리하는 유틸리티 컴포저블을 제공합니다.

NavigationBackHandler 컴포저블은 콘텐츠의 NavigationEventHandler를 만들고 이를 LocalNavigationEventDispatcherOwner에 연결합니다. 컴포저블이 화면에서 벗어나면 Compose의 DisposableEffect를 사용하여 디스패처의 dispose() 메서드를 자동으로 호출하여 리소스를 안전하게 관리합니다.

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

}

이 함수를 사용하면 현지화된 UI 하위 트리 내에서 이벤트 처리를 정확하게 제어할 수 있습니다.

@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 값을 사용하여 뒤로 동작에 대한 응답으로 UI 요소를 업데이트할 수 있으며, NavigationBackHandler을 통해 완료 및 취소를 처리할 수 있습니다.

Compose에서 뒤로 동작 또는 스와이프 가장자리에 액세스

그림 1. NavigationEvent 및 Compose로 빌드된 뒤로 탐색 예측 애니메이션

사용자가 뒤로 스와이프하는 동안 화면을 애니메이션 처리하려면 (a) NavigationEventTransitionStateInProgress인지 확인하고 (b) 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) {
        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
    }
}