Predictive Back Support

by | Oct 26, 2024

What Is Predictive Back Gesture?

Predictive Back Gesture refers to a preview that the device shows the user when they perform a back swipe gesture. As the user swipes on the screen, a preview of where this gesture will take them is shown. This animated preview allows the user to see their next destination before committing to completing the gesture.

You can see predictive back gesture in action starting from Android 13 once you’ve enabled it in the system’s developer settings. Beginning with Android 15, this feature is no longer hidden in the developer settings but enabled by default. And with Android 15 becoming available to the public, you may want to consider adding support for this feature to your app. Luckily, you just need to add android:enableOnBackInvokedCallback=“true” to your application tag in your manifest file to opt in for the predictive back gesture animations.

Starting with Android 14, you can also opt in for predictive back gesture animations at the activity level in your manifest. This will override the flag you set at the application level for this activity.

If your app uses the lower level API, OnBackInvokedCallback, then setting the android:enableOnBackInvokedCallback flag to false will cause the system to ignore calls to any implementation to OnBackInvokedCallback. It will, however, still make calls to OnBackPressedCallback, because it is backward compatible and calls the deprecated onBackPressed API.

Here is a quick look at what it looks like when the predictive back gesture is not enabled and when it is.

No predictive back gesture support
Predictive back gesture with bottom sheet
Custom predictive back animation

Intercepting System Back

In Jetpack Compose, we have the BackHandler effect composable to handle back navigation. It uses the OnBackPressedDispatcher internally and is the simplest way to intercept the system back button in Compose. You could use this to show a dialog warning the user that they will lose some data if they leave the current screen now

Be sure to update the enabled property of BackHandler to tell the system when you no longer intend to intercept the system’s back navigation.

This solution, however, is not enough to achieve the animation effect based on the user’s back gesture. The best we can achieve is to collapse it once the gesture is complete. What we need is a value that represents the user’s dragging gesture so that we can adjust the height of the Text accordingly. We’ll need to go one API level lower than the BackHandler to do that.

Add Animation Based On User’s Back Gesture

First, let’s make sure to opt-in for the predictive gesture animations in our manifest file by adding android:enableOnBackInvokedCallback=“true” to our application tag.

Here is our Text composable, which we’ll update to support animating its height based on the back gesture being performed.

val shortText = stringResource(R.string.tap_to_expand)
val longText = stringResource(R.string.long_text)

Text(
    text = if (isTextExpanded) longText else shortText,
    style = MaterialTheme.typography.bodyMedium,
    textAlign = TextAlign.Center,
    modifier = Modifier
        .fillMaxWidth()
        .clickable { isTextExpanded = !isTextExpanded }
        .animateContentSize()
        .padding(32.dp)
)

Next, we need to keep track of some state values.

  • isTextExpanded – flag to indicate if our Text composable is expanded or collapsed
  • backProgress – a Float value representing the back gesture the user is currently performing. When this value is null, it indicates that the user is not performing a back gesture.
  • textHeightExpanded – the height of our Text composable when it is expanded.
  • textHeightCollapsed – the height of our Text composable when it is collapsed.

var isTextExpanded by remember { 
    mutableStateOf(false)
}
var backProgress: Float? by remember {
    mutableStateOf(null)
}
var textHeightExpanded by remember { 
    mutableStateOf(0.dp) 
}
var textHeightCollapsed by remember {
    mutableStateOf(0.dp) 
}

With that in place, we need to be notified and react to events related to the back gesture. To do this, we create our implementation of the OnBackPressedCallback. It has a couple of handy functions that we can make use of:

  • handleOnBackProgressed() – we will use the BackEventCompat to get a progress value that tells us how far along the gesture is.
  • handleOnBackPressed() – called when the user completes the back gesture. Here, we set the state not to be expanded and clear the backProgress to indicate that the user is no longer performing a back gesture.

Until now, the Text composable has not been collapsed because the user has not committed to finishing the back navigation with their gesture; so far we’ve just been modifying the height of the Text.

  • handleOnBackCanclled() – called when the user cancels the back gesture by swiping back to the edge of their screen. Here, we just reset the backProgress state to indicate the gesture is no longer being performed.

val onBackCallback = remember {
    // We’re setting enabled to true here, because 
    // we’ll completely remove this callback when
    // we no longer intend to intercept the system back navigation
    object : OnBackPressedCallback(enabled = true) {
        override fun handleOnBackProgressed(backEvent: BackEventCompat) {
            backProgress = backEvent.progress
        }

        override fun handleOnBackPressed() {
            isTextExpanded = false
            backProgress = null
        }

        override fun handleOnBackCancelled() {
            backProgress = null
        }
    }
}

The remember call is used to retain the OnBackPressedCallback instance across recompositions.

Our callback is now ready. Next, we need to register it so that the system knows that we intend to intercept the system back navigation. To do this, we need to do the following:

  • Get a reference to OnBackPressedDispatcher. Remember that this is the backward compatible API which uses both the old OnBackPressed and the new OnBackInvokedCallback. We have access to LocalOnBackPressedDispatcherOwner, which we can use to get an instance of OnBackPressedDispatcher.
  • Set up a DisposableEffect to manage whether we want to intercept the system’s back navigation. We’ll use both our instance of OnBackPressedDispatcher and our isTextExpanded state as keys, removing the callback when the Text is no longer expanded so that when the user performs a back gesture, they go to the previous screen.

DisposableEffect executes side effects when a composable enters or leaves the composition. With its onDispose function, it allows the cleanup of resources when – removing the callback in our case.

val backPressedDispatcher = LocalOnBackPressedDispatcherOwner.current!!.onBackPressedDispatcher
DisposableEffect(backPressedDispatcher, isTextExpanded) {
    if (isTextExpanded) {
        backPressedDispatcher.addCallback(onBackCallback)
    }

    onDispose {
        onBackCallback.remove()
    }
}

The last thing we need to do is update our Text composable to adjust its height as the user performs the back gesture.

  • Set the height values of Text when it is expanded and collapsed. We do this with the onGloballyPositioned modifier to get the height of the composable after it has been drawn.

Remember to convert the values to Dp units. The value you receive is the height in pixels, which is different depending on the screen density of the device the app is running on.

  • Conditionally, add a modifier depending on whether the user is busy performing the back gesture. If they are, then we set the minimum height of Text to the collapsed height we saved previously. We then set the height of the composable to a percentage of the expanded height based on how far along the back gesture has been performed.

modifier = Modifier
    .onGloballyPositioned {
        when {
            isTextExpanded && backProgress == null -> {
                // density is a `val` assigned to LocalDensity.current
                textHeightExpanded = with(density) {
                    it.size.height.toDp()
                }
            }
            !isTextExpanded -> {
                textHeightCollapsed = with(density) {
                    it.size.height.toDp()
                }
            }
        }
    }
    .then(
        if (backProgress != null) {
            Modifier
                .heightIn(min = textHeightCollapsed)
                .height(
                    textHeightExpanded * (1f - (backProgress ?: 0f))
                )
        } else Modifier
    )

Conclusion

Predictive back gesture is a feature that shows the user a preview of their destination should they choose to commit to it. This preview can help prevent accidental navigation. Apps need to opt-in to this feature, and you can further add support by adding BackHandler to intercept and handle the user’s attempt to navigate back. We’ve even seen how you can perform animations based on the progress of the gesture that can be used to create beautiful animations.

The Essentials of Industry-Level Android App Development

Master Building Large-Scale Native Android Apps

Take this 23h online course to learn everything it takes to become an industry-ready native Android developer. Learn about multi-module architecture, Gradle, Jetpack Compose, authentication, offline-first development and more.

Read More