조건부 전환 레시피

이 레시피에서는 transitionSpec 및 popTransitionSpec을 사용하여 Navigation 3에서 경로 종속 화면 전환을 만드는 방법을 보여줍니다. 슬라이드 방향 (오른쪽, 왼쪽, 하단, 상단)은 initialState 및 targetState 탐색 키 쌍을 기반으로 조건부로 선택됩니다.

작동 방식

  • 경로 정의: 탐색 대상 (Step1, Step2, Step3, Step4)은 NavKey를 구현하고 @Serializable로 표시된 봉인된 클래스 계층 구조를 사용하여 정의됩니다.
  • 조건부 전달 전환 (transitionSpec): (initialKey to targetKey) 쌍을 일치시켜 슬라이드 애니메이션의 방향을 결정합니다.
    • Step1 $\rightarrow$ Step2: 왼쪽으로 스와이프
    • Step2 $\rightarrow$ Step3: 위로 스와이프
    • Step3 $\rightarrow$ Step4: 오른쪽으로 스와이프
    • Step4 $\rightarrow$ Step1: 하단으로 슬라이드 (흐름 다시 시작)
  • 조건부 팝 전환 (popTransitionSpec): 뒤로 이동하거나 백스택을 지울 때 역방향 슬라이드 방향을 처리합니다.
  • 백스택 제어: 원활한 상단 슬라이드 전환을 실행하는 동안 마지막 단계에서 탐색 스택을 지우는 방법을 보여줍니다 (backStack.clear() & backStack.add(Step1))
package com.example.nav3recipes.conditionaltransitions

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.ContentTransform
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
import com.example.nav3recipes.content.ContentGreen
import com.example.nav3recipes.content.ContentOrange
import com.example.nav3recipes.content.ContentPurple
import com.example.nav3recipes.content.ContentRed
import com.example.nav3recipes.sharedviewmodel.toContentKey
import com.example.nav3recipes.ui.setEdgeToEdgeConfig
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.serialization.Serializable

@Serializable
sealed class Step(val firstButtonTitle: String, val secondButtonTitle: String) : NavKey

@Serializable
data object Step1 : Step("Home", "Swipe left")

@Serializable
data object Step2 : Step("Swipe right", "Swipe up")

@Serializable
data object Step3 : Step("Swipe down", "Swipe right")

@Serializable
data object Step4 : Step("Swipe left", "Swipe down")

@AndroidEntryPoint
class ConditionalTransitionsActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setEdgeToEdgeConfig()

        setContent {
            Scaffold { paddingValues ->
                val backStack = rememberNavBackStack(Step1)

                NavDisplay(
                    backStack = backStack,
                    modifier = Modifier.padding(paddingValues),
                    onBack = backStack::removeLastOrNull,
                    entryProvider = entryProvider {
                        entry<Step1> {
                            ContentGreen(title = "Screen 1") {
                                StepContent(
                                    step = it,
                                    onNext = { backStack += Step2 },
                                    onBack = ::finish, // closing the activity
                                )
                            }

                        }
                        entry<Step2> {
                            ContentRed(title = "Screen 2") {
                                StepContent(
                                    step = it,
                                    onNext = { backStack += Step3 },
                                    onBack = backStack::removeLastOrNull,
                                )
                            }
                        }
                        entry<Step3> {
                            ContentOrange(title = "Screen 3") {
                                StepContent(
                                    step = it,
                                    onNext = { backStack += Step4 },
                                    onBack = backStack::removeLastOrNull,
                                )
                            }
                        }
                        entry<Step4> {
                            ContentPurple(title = "Screen 4") {
                                StepContent(
                                    step = it,
                                    onNext = {
                                        backStack.clear()
                                        backStack.add(Step1)
                                    },
                                    onBack = backStack::removeLastOrNull,
                                )
                            }
                        }
                    },
                    transitionSpec = {
                        val initialKey = initialState.entries.lastOrNull()?.contentKey
                        val targetKey = targetState.entries.lastOrNull()?.contentKey

                        when (initialKey to targetKey) {
                            Step1.toContentKey() to Step2.toContentKey() -> swipeLeft()
                            Step2.toContentKey() to Step3.toContentKey() -> swipeUp()
                            Step3.toContentKey() to Step4.toContentKey() -> swipeRight()
                            Step4.toContentKey() to Step1.toContentKey() -> swipeDown()
                            else -> swipeRight()
                        }
                    },
                    popTransitionSpec = {
                        val initialKey = initialState.entries.lastOrNull()?.contentKey
                        val targetKey = targetState.entries.lastOrNull()?.contentKey

                        when (initialKey to targetKey) {
                            Step4.toContentKey() to Step1.toContentKey() -> swipeDown() // via backstack clearing
                            Step2.toContentKey() to Step1.toContentKey() -> swipeRight()
                            Step3.toContentKey() to Step2.toContentKey() -> swipeDown()
                            Step4.toContentKey() to Step3.toContentKey() -> swipeLeft()
                            else -> swipeLeft()
                        }
                    },
                )
            }
        }
    }
}

@Composable
fun StepContent(step: Step, onNext: () -> Unit, onBack: () -> Unit) {
    Column(
        verticalArrangement = Arrangement.SpaceEvenly,
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier.fillMaxSize()
    ) {
        Row(horizontalArrangement = Arrangement.SpaceEvenly, modifier = Modifier.fillMaxWidth()) {
            ElevatedButton(onBack) { Text(text = step.firstButtonTitle) }
            ElevatedButton(onNext) { Text(text = step.secondButtonTitle) }
        }
    }
}

private fun swipeLeft(): ContentTransform =
    slideInHorizontally(initialOffsetX = { it }) togetherWith slideOutHorizontally(targetOffsetX = { -it })

private fun swipeUp(): ContentTransform =
    slideInVertically(initialOffsetY = { it }) togetherWith slideOutVertically(targetOffsetY = { -it })

private fun swipeRight(): ContentTransform =
    slideInHorizontally(initialOffsetX = { -it }) togetherWith slideOutHorizontally(targetOffsetX = { it })

private fun swipeDown(): ContentTransform =
    slideInVertically(initialOffsetY = { -it }) togetherWith slideOutVertically(targetOffsetY = { it })