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

אפשר להרחיב את המחלקה המופשטת 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 מופעלות לפני הפונקציות לטיפול ב-PRIORITY_DEFAULT. בתוך כל קבוצת עדיפות, הפונקציות לטיפול בבקשות מופעלות בסדר Last-In, First-Out (LIFO) – הפונקציה לטיפול בבקשות שהוספה לאחרונה מופעלת ראשונה.

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

ב-Jetpack פיתוח נייטיב, הספרייה מספקת קומפוזבילי לשימוש כדי לנהל את היררכיית ה-dispatcher.

רכיב ה-Composable‏ NavigationBackHandler יוצר NavigationEventHandler לתוכן שלו ומקשר אותו ל-LocalNavigationEventDispatcherOwner. הוא משתמש ב-DisposableEffect של Compose כדי לקרוא אוטומטית לשיטת DisposableEffect של ה-dispatcher כשהקומפוננטה הניתנת להרכבה יוצאת מהמסך, וכך מנהל את המשאבים בצורה בטוחה.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: מספר ממשי (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) {
        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
    }
}