טיפול בתנועות חזרה ובחיזוי אנימציה של תנועת החזרה

אפשר להרחיב את המחלקה המופשטת 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 מחברת את ה-handler ל-dispatcher:

navigationEventDispatcher.addHandler(myHandler)

קוראים ל-myHandler.remove() כדי להסיר את ה-handler מה-dispatcher:

myHandler.remove()

הפונקציות לטיפול באירועים מופעלות לפי עדיפות, ואז לפי עדכניות. כל ה-handlers של PRIORITY_OVERLAY נקראים לפני ה-handlers של PRIORITY_DEFAULT. בתוך כל קבוצת עדיפות, ה-handlers מופעלים בסדר "נכנס אחרון, יוצא ראשון" (LIFO) – ה-handler שנוסף אחרון מופעל ראשון.

יירוט של חזרה אחורה באמצעות Jetpack Compose

ב-Jetpack Compose, הספרייה מספקת פונקציה קומפוזבילית לניהול ההיררכיה של ה-dispatcher.

הפונקציה הקומפוזבילית NavigationBackHandler יוצרת NavigationEventHandler עבור התוכן שלה ומקשרת אותו אל LocalNavigationEventDispatcherOwner. היא משתמשת ב-DisposableEffect של Compose כדי להפעיל באופן אוטומטי את השיטה dispose() של הדיספצ'ר כשהרכיב ה-composable יוצא מהמסך, וכך לנהל את המשאבים בצורה בטוחה.

@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.

איך לגשת לתנועת החזרה או להחלקה מהקצה ב-Compose

איור 1. אנימציה של חיזוי החזרה שנוצרה באמצעות NavigationEvent ו-Compose.

כדי להוסיף אנימציה למסך בזמן שהמשתמש מחליק אחורה, צריך (א) לבדוק אם NavigationEventTransitionState הוא InProgress, ו-(ב) לצפות בהתקדמות ובמצב של קצה ההחלקה באמצעות rememberNavigationEventState:

  • ‫progress: מספר ממשי (float) מ-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
    }
}