Menangani gestur kembali dan animasi kembali prediktif

Anda dapat memperluas class abstrak NavigationEventHandler untuk menangani peristiwa navigasi di seluruh platform. Class ini menyediakan metode yang sesuai dengan siklus proses gestur navigasi.

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
    }
}

Fungsi addHandler menghubungkan pengendali ke pengirim:

navigationEventDispatcher.addHandler(myHandler)

Panggil myHandler.remove() untuk menghapus pengendali dari dispatcher:

myHandler.remove()

Handler dipanggil berdasarkan prioritas, lalu berdasarkan kebaruan. Semua pengendali PRIORITY_OVERLAY dipanggil sebelum pengendali PRIORITY_DEFAULT dipanggil. Dalam setiap grup prioritas, handler dipanggil dalam urutan Last-In, First-Out (LIFO) — handler yang baru ditambahkan akan dipanggil terlebih dahulu.

Mencegat kembali dengan Jetpack Compose

Untuk Jetpack Compose, library menyediakan composable utilitas untuk mengelola hierarki dispatcher.

Composable NavigationBackHandler membuat NavigationEventHandler untuk kontennya dan menautkannya ke LocalNavigationEventDispatcherOwner. Composable ini menggunakan DisposableEffect Compose untuk otomatis memanggil metode dispose() dispatcher saat composable keluar dari layar, sehingga mengelola resource dengan aman.

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

}

Fungsi ini memungkinkan Anda mengontrol penanganan peristiwa secara tepat dalam subpohon UI yang dilokalkan.

@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()
        }
    )
}

Contoh ini menunjukkan cara mengamati pembaruan gestur kembali prediktif menggunakan NavigationEventTransitionState. Nilai progress dapat digunakan untuk memperbarui elemen UI sebagai respons terhadap gestur kembali, sekaligus menangani penyelesaian dan pembatalan melalui NavigationBackHandler.

Mengakses gestur kembali atau menggeser tepi di Compose

Gambar 1. Animasi kembali prediktif yang dibuat dengan NavigationEvent dan Compose.

Untuk menganimasikan layar saat pengguna menggeser kembali, Anda harus (a) memeriksa apakah NavigationEventTransitionState adalah InProgress, dan (b) mengamati progres dan status tepi geser dengan rememberNavigationEventState:

  • progress: Float dari 0.0 hingga 1.0 yang menunjukkan seberapa jauh pengguna telah menggeser.
  • swipeEdge: Konstanta bilangan bulat (EDGE_LEFT atau EDGE_RIGHT) yang menunjukkan tempat dimulainya gestur.

Cuplikan berikut adalah contoh sederhana cara menerapkan animasi penskalaan dan pergeseran:

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
    }
}