Skip to content

Predictive Back Gesture animation doesn't and can't implement material design spec to match native behavior #153577

Open

Description

@PaulGrandperrin

Steps to reproduce

Use PredictiveBackPageTransitionsBuilder()

Expected results

The same animations and behavior as the native apps and system.
Spec: https://developer.android.com/design/ui/mobile/guides/patterns/predictive-back

Actual results

In the "pre-commit" part of the animation:

  1. the front page abruptly disappears when swiping across 35% of the screen
  2. the front and back pages ignore if the gesture started from the left or right edge
  3. the front and back pages don't go up and down following finger

In the "post-commit" part of the animation:

  1. the animation goes way too fast (<100ms, should be ~300ms)
  2. the animation blocks all user inputs for about 700ms after the animation is visually finished
  3. the animation is the same as the "pre-commit" one, which is not what happens in the rest of Android and in the spec.

1. and 6. can be worked around by writing a custom PageTransitionBuilder.
2. and 3. can be partially worked around: only for the front page.
4. and 5. cannot be worked around without editing the SDK source code itself.

Flutter Doctor output

Doctor output
flutter doctor -v                                                                                                                                            devenv-shell-env 
[✓] Flutter (Channel master, 3.24.0-1.0.pre.605, on NixOS 24.05 (Uakari) 6.8.12, locale en_US.UTF-8)
    • Flutter version 3.24.0-1.0.pre.605 on channel master at /home/paulg/Repos/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision bced008679 (14 hours ago), 2024-08-15 22:29:24 -0400
    • Engine revision a8fefc8118
    • Dart version 3.4.3
    • DevTools version 2.38.0

[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.3)
    • Android SDK at /nix/store/nzwnj7rq8a6cvzbsszf4n9zfvx101zm5-androidsdk/libexec/android-sdk
    • Platform android-34, build-tools 30.0.3
    • ANDROID_HOME = /nix/store/nzwnj7rq8a6cvzbsszf4n9zfvx101zm5-androidsdk/libexec/android-sdk
    • ANDROID_SDK_ROOT = /nix/store/nzwnj7rq8a6cvzbsszf4n9zfvx101zm5-androidsdk/libexec/android-sdk
    • Java binary at: /nix/store/l7pwy1rxdrb13svqandm720d06fycd8c-openjdk-17.0.11+9/lib/openjdk/bin/java
    • Java version OpenJDK Runtime Environment (build 17.0.11+9-nixos)
    • All Android licenses accepted.

[✓] Chrome - develop for the web
    • CHROME_EXECUTABLE = /nix/store/cg5bvzxml5bbdw8wlrywb6q7hpdjq0ij-google-chrome-127.0.6533.88/bin/google-chrome-stable

[✓] Linux toolchain - develop for Linux desktop
    • clang version 16.0.6
    • cmake version 3.29.6
    • ninja version 1.12.1
    • pkg-config version 0.29.2

[!] Android Studio (not installed)
    • Android Studio not found; download from https://developer.android.com/studio/index.html
      (or visit https://flutter.dev/docs/get-started/install/linux#android-setup for detailed instructions).

[✓] Connected device (3 available)
    • Pixel 7 Pro (mobile) • 192.168.1.13:43591 • android-arm64  • Android 15 (API 35)
    • Linux (desktop)      • linux              • linux-x64      • NixOS 24.05 (Uakari) 6.8.12
    • Chrome (web)         • chrome             • web-javascript • Google Chrome 127.0.6533.88

[✓] Network resources
    • All expected network resources are available.

! Doctor found issues in 1 category.

Activity

  1. PaulGrandperrin commented on Aug 16, 2024

    @PaulGrandperrin
    Author

    I wrote a few more detailed comments on this PR while investigating: #141373

  2. changed the title [-]Predictive Back Gesture animation can't implement material design spec [/-] [+]Predictive Back Gesture animation doesn't and can't implement material design spec [/+] on Aug 16, 2024
  3. changed the title [-]Predictive Back Gesture animation doesn't and can't implement material design spec [/-] [+]Predictive Back Gesture animation doesn't and can't implement material design spec to match native behavior[/+] on Aug 16, 2024
  4. PaulGrandperrin commented on Aug 17, 2024

    @PaulGrandperrin
    Author

    I guess it's important to note that I'm using Android 15 (API 35), when the current SDK implementation is based on Android 14 (API 34): https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/material/predictive_back_page_transitions_builder.dart#L181

    I don't remember how this animation looked on this version, so maybe flutter's implementation was more faithful than what I'm describing here.

  5. added
    in triagePresently being triaged by the triage team
    on Aug 19, 2024
  6. huycozy commented on Aug 19, 2024

    @huycozy
    Member

    Hi @PaulGrandperrin

    the front page abruptly disappears when swiping across 35% of the screen

    I checked this on Flutter and Android native apps and I can see different swipe gesture distances. In Flutter app, I need to swipe longer than on Android native.

    Demo
    Flutter Android native
    Screen.Recording.2024-08-19.at.15.12.53.mov
    Screen.Recording.2024-08-19.at.15.24.56.mov

    the front and back pages ignore if the gesture started from the left or right edge
    the front and back pages don't go up and down following finger

    Can you please be more specific on these points?

  7. added
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Aug 19, 2024
  8. PaulGrandperrin commented on Aug 19, 2024

    @PaulGrandperrin
    Author

    I checked this on Flutter and Android native apps and I can see different swipe gesture distances. In Flutter app, I need to swipe longer than on Android native.

    To be more specific, when BackEvent.progress becomes bigger than 0.35, animation.valuebecomes smaller than 0.65, and when this happens fadeTween will select the first TweenSequenceItem which goes from 0.0 to 0.0. Since this controls the opacity, the front page abruptly disappear at progress = 35%. Here's the code:

        final Animatable<double> fadeTween =
            TweenSequence<double>(<TweenSequenceItem<double>>[
          TweenSequenceItem<double>(
            tween: Tween<double>(begin: 0.0, end: 0.0), 
            weight: _weightForStartState,
          ),
          TweenSequenceItem<double>(
            tween: Tween<double>(
              begin: _opacityStartTransition,
              end: _opacityFullyOpened,
            ),
            weight: _weightForEndState,
          ),
        ]);

    I have no idea was it was supposed to originally, but there's clearly at least a typo in the range values.

  9. removed
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Aug 19, 2024
  10. PaulGrandperrin commented on Aug 19, 2024

    @PaulGrandperrin
    Author

    Can you please be more specific on these points?

    I think a lot can be explained by the differences between Android 14 and 15. The current code was tested against 14, and I now realize that on this version, the animation mostly works for several reasons:

    • on 14, the animation is simpler and looks like the pre-commit and post-commit phases are part of one bigger animation
    • on 14, there's something that nudge the progress values quickly around 35%, making it difficult to see that the front page vanishes abruptly.
    • on 14, I guess the animation doesn't react to the finger going up and down

    However, I strongly suppose the 700ms of unresponsive app after a route pop is still there.

    I captured on my Android 15 phone, what the current code looks like: https://drive.google.com/file/d/1ZfraE_JL6PA27aD0gMij3kpzcvOnOEcY/view?usp=sharing
    You can see that

    • the abrupt opacity drop
    • the extremely fast postCommit animation
    • how the app becomes unresponsive for almost a second after the route is popped
    • no response to up and down
    • same animation from left and right

    Here's how it looks on this PR: https://drive.google.com/file/d/1ZiOU_fUCc6v_npc2rTg405iyWSSXdDHj/view?usp=sharing
    I also show what the animation looks like natively in the Settings.

  11. PaulGrandperrin commented on Aug 19, 2024

    @PaulGrandperrin
    Author

    One important thing is that on Android 15, which will be released soon, the predictive back gestures are enabled by default.

    So a lot of people will soon be exposed to it, and the flutter app devs won't be able to fix those issues, even with a custom transition, because part of the responsible code is in TransitionRoute, like the curve controlling the postCommit animation pace.

  12. PaulGrandperrin commented on Aug 19, 2024

    @PaulGrandperrin
    Author

    To better see how the animation adapts from which side it was started: https://drive.google.com/file/d/1ZiqAj-TErJsZuU5Bg2i8gLfgvjBV0Kvp/view?usp=sharing

  13. PaulGrandperrin commented on Aug 19, 2024

    @PaulGrandperrin
    Author

    Oh and I'm thinking about another reason why it didn't shock anyone until now, on the Android emulator the frame rate is already often choppy and targets I guess 60fps.

    On a real 120fps phone, when building in release mode, the issues I mentioned are a lot more striking.

  14. huycozy commented on Aug 20, 2024

    @huycozy
    Member

    Thanks for providing more information. I don't have an Android 15 device now, I will label this for other's input.

  15. removed
    in triagePresently being triaged by the triage team
    on Aug 20, 2024
  16. 8 remaining items

  17. justinmc commented on Aug 26, 2024

    @justinmc
    Contributor

    On the topic of Shared Element vs. Full Screen Surfaces, it looks like Android has now defaulted to Shared Element. This is what I see if I create a plain Compose app on API 35:

    Video of native API 35 default transition between Activities
    Screencast.from.2024-08-26.12-44-42.webm

    Also, I don't see the Full Screen Surfaces animation anywhere if I look for it in the system Android 15 apps.

    So I think that Flutter should default to Shared Element transition and not worry about Full Screen Surfaces. Apologies for previously saying the opposite. It looks like the default transition on Android has changed from then and now.

    I think Shared Element Transition is currently under development in issue flutter/packages#6321. Am I right, @justinmc?

    That PR is a little stale at this point, but my intention was to make the existing OpenContainer transitions work with predictive back. My implementation probably was intended to look like the Shared Element transition mentioned above, but where the popped page animates to look like it's coming from some widget on the previous page.


    So @maRci002 since you still have your old code, are you interested in opening a PR to update the PredictiveBackPageTransition animation to look like Shared Element? It seems like #153635 is not intending to land.

  18. maRci002 commented on Aug 26, 2024

    @maRci002
    Contributor

    Also, I don't see the Full Screen Surfaces animation anywhere if I look for it in the system Android 15 apps.

    My observation is that Android native (using Intents) applies the Shared Element transition; however, when Compose itself animates, it applies the Full Screen Surfaces animation. The official predictive back codelab uses the Full Screen Surfaces animation.

    I have created a Compose application where both can be tried out:

    MainActivity.kt — don't forget to toggle the useNativeAnimations variable to try out both:

    MainActivity.kt
    package com.example.compose_predictive_back
    
    import android.content.Intent
    import android.os.Bundle
    import android.window.BackEvent
    import androidx.activity.BackEventCompat
    import androidx.activity.ComponentActivity
    import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
    import androidx.activity.compose.setContent
    import androidx.activity.enableEdgeToEdge
    import androidx.compose.foundation.layout.Arrangement
    import androidx.compose.foundation.layout.Column
    import androidx.compose.foundation.layout.fillMaxSize
    import androidx.compose.material3.Button
    import androidx.compose.material3.MaterialTheme
    import androidx.compose.material3.Surface
    import androidx.compose.material3.Text
    import androidx.compose.material3.darkColorScheme
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.getValue
    import androidx.compose.runtime.mutableFloatStateOf
    import androidx.compose.runtime.remember
    import androidx.compose.runtime.setValue
    import androidx.compose.ui.Alignment
    import androidx.compose.ui.Modifier
    import androidx.navigation.compose.NavHost
    import androidx.navigation.compose.composable
    import androidx.navigation.compose.rememberNavController
    import com.example.compose_predictive_back.ui.theme.Compose_predictive_backTheme
    import kotlinx.coroutines.CoroutineScope
    import kotlinx.coroutines.Dispatchers
    import kotlinx.coroutines.delay
    import kotlinx.coroutines.launch
    
    class MainActivity : ComponentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            enableEdgeToEdge()
            setContent {
                Compose_predictive_backTheme {
                    val useNativeAnimations = true;
    
                    if (useNativeAnimations) {
                        FirstScreen(onClick = {
                            startActivity(
                                Intent(
                                    this@MainActivity,
                                    SecondActivity::class.java
                                )
                            )
                        })
                    } else {
                        Navigator()
                    }
                }
            }
        }
    }
    
    class SecondActivity : ComponentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                SecondScreen()
            }
        }
    }
    
    @Composable
    fun Navigator() {
        val navController = rememberNavController()
    
        NavHost(
            navController = navController,
            startDestination = "first_screen",
            // enterTransition = { slideInHorizontally { it / 2 } },
            // exitTransition = { slideOutHorizontally { -it / 2 } },
        ) {
            composable("first_screen") {
                FirstScreen(onClick = { navController.navigate("second_screen") })
            }
            composable("second_screen") {
                SecondScreen()
            }
        }
    }
    
    @Composable
    fun FirstScreen(onClick: () -> Unit) {
        Surface(color = MaterialTheme.colorScheme.background, modifier = Modifier.fillMaxSize()) {
            Column(
                modifier = Modifier.fillMaxSize(),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Button(onClick = onClick) {
                    Text("Go to Second Screen")
                }
            }
        }
    }
    
    @Composable
    fun SecondScreen() {
        MaterialTheme(colorScheme = darkColorScheme()) {
            Surface(color = MaterialTheme.colorScheme.background, modifier = Modifier.fillMaxSize()) {
                Column(
                    modifier = Modifier.fillMaxSize(),
                    verticalArrangement = Arrangement.Center,
                    horizontalAlignment = Alignment.CenterHorizontally
                ) {
                    Text(text = "Hello, Predictive back!")
                    BackControlButton()
                }
            }
        }
    }
    
    
    @Composable
    fun BackControlButton() {
        val backPressedDispatcher =
            LocalOnBackPressedDispatcherOwner.current!!.onBackPressedDispatcher
    
        var progress by remember { mutableFloatStateOf(0f) }
    
        Button(onClick = {
            CoroutineScope(Dispatchers.Main).launch {
                backPressedDispatcher.dispatchOnBackStarted(
                    BackEventCompat(0.1F, 0.1F, progress, BackEvent.EDGE_LEFT)
                )
    
                delay(timeMillis = 500)
    
                for (step in 1..95) {
                    val updatedProgress = step / 100f
                    backPressedDispatcher.dispatchOnBackProgressed(
                        BackEventCompat(0.1F, 0.1F, updatedProgress, BackEvent.EDGE_LEFT)
                    )
                    progress = updatedProgress
                    delay(50)
                }
    
                delay(timeMillis = 500)
    
                backPressedDispatcher.dispatchOnBackProgressed(
                    BackEventCompat(0.0F, 0.0F, 0F, BackEvent.EDGE_LEFT)
                )
                progress = 0F
            }
        }) {
            Text("Progress: ${(progress * 100).toInt()}%")
        }
    }

    AndroidManifest.xml: Don't forget to add android:enableOnBackInvokedCallback="true" and also register the second activity like this: <activity android:name=".SecondActivity" />

    In gradle/libs.versions.toml, increase the navigationCompose to 2.8.0-beta02 (this is the minimum version required to support the predictive back feature) or higher, such as 2.8.0-alpha08.

    Compose - androidx.navigation:navigation-compose:2.8.0-alpha08

    compose_predictive_back.mp4

    Native (using Intents)

    native_predictive_back.mp4

    So @maRci002 since you still have your old code, are you interested in opening a PR to update the PredictiveBackPageTransition animation to look like Shared Element? It seems like #153635 is not intending to land.

    Yes, I am interested in opening a PR. The PredictiveBackPageTransitionsBuilder could be made more generic, for instance, by having its own transition builder. The default could be PredictiveBackPageTransitionsBuilder.sharedElementTransitionBuilder, but the old one, PredictiveBackPageTransitionsBuilder.fullScreenSurfacesTransitionBuilder, would still be available.

  19. justinmc commented on Sep 5, 2024

    @justinmc
    Contributor

    @maRci002 Ah good catch, I guess ideally we should have both. Your approach looks good. I'm happy to help review if you open a PR.

  20. NikosTsesmelis commented on Jan 20, 2025

    @NikosTsesmelis
    Contributor

    I would like to add that the corners on the native transition are rounded in my device but in the flutter implementation they are not

  21. self-assigned this
    on Mar 24, 2025
  22. justinmc commented on Apr 9, 2025

    @justinmc
    Contributor

    This will be partially fixed by #154718. The transition of the incoming route (delegatedTransition) will happen in a separate PR.

    For future reference he delegatedTransition seems to have three components:

    1. X translation
    2. Scale
    3. Dimming/opacity (the incoming page is darker in both light and dark modes).

    Edit Some of this animation happens before commit, and currently, delegatedTransform doesn't run until after the commit. There will probably be some kind of infrastructure needed to support that. I assume just using _PredictiveBackGestureDetector in the delegatedTransition won't be enough.

  23. added
    ⏳Bot is counting down the days until it unassigns the issue
    on Aug 13, 2025
  24. flutter-triage-bot commented on Aug 13, 2025

    @flutter-triage-bot

    This issue is assigned to @justinmc but has had no recent status updates. Please consider unassigning this issue if it is not going to be addressed in the near future. This allows people to have a clearer picture of what work is actually planned. Thanks!

  25. removed their assignment
    on Aug 15, 2025
  26. removed
    ⏳Bot is counting down the days until it unassigns the issue
    on Aug 15, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work listf: routesNavigator, Router, and related APIs.frameworkflutter/packages/flutter repository. See also f: labels.platform-androidAndroid applications specificallyteam-frameworkOwned by Framework teamtriaged-frameworkTriaged by Framework team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions