Conditional Transitions Recipe

Công thức này minh hoạ cách tạo hiệu ứng chuyển đổi màn hình phụ thuộc vào tuyến đường trong Navigation 3 bằng cách sử dụng transitionSpec và popTransitionSpec. Hướng trượt (phải, trái, dưới cùng, trên cùng) được chọn có điều kiện dựa trên các cặp phím điều hướng initialState và targetState.

Cách hoạt động

  • Định nghĩa tuyến đường: Các đích đến điều hướng (Step1, Step2, Step3, Step4) được xác định bằng cách sử dụng một hệ phân cấp lớp kín triển khai NavKey và được đánh dấu bằng @Serializable.
  • Chuyển tiếp có điều kiện (transitionSpec): So khớp các cặp (initialKey to targetKey) để xác định hướng của ảnh động trên trang trình bày:
    • Step1 $\rightarrow$ Step2: Vuốt sang trái
    • Step2 $\rightarrow$ Step3: Vuốt lên
    • Step3 $\rightarrow$ Step4: Vuốt sang phải
    • Step4 $\rightarrow$ Step1: Vuốt xuống dưới cùng (khởi động lại quy trình)
  • Conditional Pop Transitions (popTransitionSpec): Xử lý hướng trượt ngược khi điều hướng quay lại hoặc khi xoá ngăn xếp lui.
  • Backstack Control (Kiểm soát ngăn xếp lui): Minh hoạ việc xoá ngăn xếp điều hướng ở bước cuối cùng (backStack.clear() và backStack.add(Step1)) trong khi thực hiện một quá trình chuyển đổi liền mạch từ trên xuống.
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 })