-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Predictive back support for routes #141373
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
One aspect I'm considering is how to best align with the Predictive Back design, specifically the "Back Preview" experience. The gesture-controlled route should incorporate two types of transitions. One type of transition would occur when the route is controlled programmatically, such as through the use of Considering that various Material components like Bottom sheets, Side sheets, and Search include back gesture animations, it is advisable to develop a generic, reusable component for managing these transitions. Definitely needed the two different transitions; otherwise, this happens: predictive_back_single_animation.webm |
|
This pull request seems prepared for review. Currently, it lacks documentation and tests, which I plan to add once we confirm that the API is satisfactory. This also applies to the engine part flutter/engine#49093 |
justinmc
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks again for the PR and sorry for my delay in getting set up to run this! Overall the approach looks good and it looks and feels good when I try it out locally with the engine PR.
A few thoughts that came up as I was reviewing this:
Currently, a user has to explicitly use AndroidBackGesturePageTransitionsBuilder in order to get the predictive back transition, right? I wonder if it should be built into any of the existing route transitions. That's probably beyond the scope of this PR, though. Do you know if this kind of predictive back transition is the default in native Android?
Do you have any specific native app that you have been using to try to match this to? If I try the predictive back transition on Google Calendar it feels more stiff than this one, and there are probably other small differences. Maybe there should be a fadeout animation. These are small details, though.
What if someone wants to make a route transition that animates itself into some UI element on the parent route? Such as in the predictive back spec (video below). Maybe you should include an example of doing something like that in the examples directory and link it in your docs.
predictive-back-video-1-calendar-event-example.mp4
@goderbauer FYI in case you want to review this PR related to navigation.
37cc128 to
0c355ce
Compare
Yes, to enable predictive back transitions, developers should explicitly use
I don't really know the predictive back transitions, as recommended by Android's Material Design guidelines, have been incorporated in my implementation. However, effects like fading or barrier colors for better visual appeal haven't been added. In my demo, I cheated by applying a high contrast, using a dark page on top of a light page.
These kinds of animations should be implemented using the package available at https://pub.dev/packages/animations. I've made every effort to ensure the solution is as modular and flexible as possible. For instance, there's the |
|
That all sounds good, thanks for the detailed response! I'm going to check on 1. the default route transitions on native Android and 2. I'll play around with the animations package and see how easy it would be to tie into predictive back. |
0c355ce to
0c21751
Compare
|
I created a quick native Android app to check the route transitions that you get out of the box, gif below. This was after setting I'll try to make sure that we can do this kind of transition as well as the kind with the |
In that case, you would want to add a new Anyway, when I change the Here is the video showing the reverse animation of the first screen. It demonstrates the topmost route being popped, which triggers the Detailspredictive_back_parent_reverse_animation.mp4 |
|
You're saying that in your video, the "First Screen" is unexpectedly also animating during the back gesture? I do see that. I'm currently trying to gather some info on Android's default transition and will post back with more details later. |
|
I figured out what the transitions should be. The default should be the same default that we see on Android right now, which is a resizing and fading. I don't think there is an official spec for this, but I recorded a gif of it below. And like I thought, we should make sure that the shared element transition is doable with predictive back. That animation does have a spec, and below is the video from it.
|
justinmc
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the main blockers that I see now are:
- The use of the protected
controller.animation, commented on below. I think this is an architecture problem we'll have to figure out. - The AndroidBackGestureTransition should match the animation that I posted a gif of.
I think we'll also need to make AndroidBackGestureTransition the default transition on Android, but I can do that in a separate PR, because it may be a breaking change.
For the shared element transitions that I mentioned before, I think I can add that ability to the animations package as a separate PR built on top of this. It looks possible using your API.
Also, reminder that this PR will need tests at some point.
I really appreciate you working with me to get this supported!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be the default for Android in PageTransitionsTheme, but I guess we should probably do that in a separate PR, since it could be a breaking change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reminder to add docs here and for the public members.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are calling addObserver in initState of AndroidBackGestureDetector. Is it guaranteed that the observer that you want will be called here?
I wonder if there is a better way for this use case. Maybe by finding the current route of the navigator or something?
handlePopRoute (above) also suffers from this, though, so this is an existing problem, and I guess it's acceptable if there is no easy way around it right now.
|
@maRci002 Are you still available to work on this PR? I'm happy to take over work on this project myself if you're busy. |
Yes, this week I am going to address the unresolved conversations. |
|
Great, thanks and keep me posted! |
0c21751 to
fef8fd3
Compare
|
@maRci002 Can you push a merge commit? I think a bunch of checks are stuck. I'll post a review soon, thanks for the quick turnaround! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@maRci002 Thanks so much for the update and for putting in the tedious work to match the animations. Some bugs that I found:
Back button doesn't work
If I try your example code at the top of this PR, just tapping the back button doesn't work. Sometimes nothing happens at all, and sometimes there is some weird jumpiness.
XRecorder_05032024_105350.mp4
No opacity animation
When I swipe back slowly, there is a point at which the top route suddenly disappears. Looking at native Android it seems like there should be an opacity animation. It happens at the point where releasing the gesture would commit the pop Actually it seems to happen after that point. To me it seems to not be driven by the back gesture (so when the back gesture reaches the commit point, the opacity animation runs in a fixed amount of time).
XRecorder_05032024_104929.mp4
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should watch out for this being a breaking change. If so it will be a very easy migration for users, though.
Manual roll Flutter from dbdcead to 89ea492 (54 revisions) Manual roll requested by [email protected] flutter/flutter@dbdcead...89ea492 2024-03-28 [email protected] Roll Flutter Engine from f71e5ad8586b to a396dc1a03a9 (3 revisions) (flutter/flutter#145928) 2024-03-28 [email protected] Roll Flutter Engine from 043af350ae85 to f71e5ad8586b (1 revision) (flutter/flutter#145919) 2024-03-28 [email protected] Refactor skp_generator_tests (flutter/flutter#145871) 2024-03-28 [email protected] Roll Flutter Engine from 7c9d5adb6ff8 to 043af350ae85 (2 revisions) (flutter/flutter#145917) 2024-03-28 [email protected] Update `TabBar` and `TabBar.secondary` to use indicator height/color M3 tokens (flutter/flutter#145753) 2024-03-28 [email protected] Roll Packages from e6b3e11 to 924c7e6 (5 revisions) (flutter/flutter#145915) 2024-03-28 [email protected] Add `viewId` to `TextInputConfiguration` (flutter/flutter#145708) 2024-03-28 [email protected] Roll Flutter Engine from 9df2d3a0778e to 7c9d5adb6ff8 (3 revisions) (flutter/flutter#145909) 2024-03-28 [email protected] Manual roll Flutter Engine from c602abdbae16 to 9df2d3a0778e (10 revisions) (flutter/flutter#145903) 2024-03-28 98614782+auto-submit[bot]@users.noreply.github.com Reverts "Roll Flutter Engine from c602abdbae16 to 922c7b133bc2 (7 revisions) (#145877)" (flutter/flutter#145901) 2024-03-28 98614782+auto-submit[bot]@users.noreply.github.com Reverts "Roll Flutter Engine from 922c7b133bc2 to b3516c4c5683 (1 revision) (#145879)" (flutter/flutter#145900) 2024-03-28 [email protected] Roll Flutter Engine from 922c7b133bc2 to b3516c4c5683 (1 revision) (flutter/flutter#145879) 2024-03-28 [email protected] Roll Flutter Engine from c602abdbae16 to 922c7b133bc2 (7 revisions) (flutter/flutter#145877) 2024-03-28 [email protected] Remove deprecated `TextTheme` members (flutter/flutter#139255) 2024-03-27 [email protected] [WIP] Predictive back support for routes (flutter/flutter#141373) 2024-03-27 [email protected] Roll Flutter Engine from 73c145c9ac3a to c602abdbae16 (1 revision) (flutter/flutter#145865) 2024-03-27 [email protected] Roll Flutter Engine from d65662541682 to 73c145c9ac3a (8 revisions) (flutter/flutter#145862) 2024-03-27 [email protected] Roll Flutter Engine from b7dddee939f2 to d65662541682 (2 revisions) (flutter/flutter#145851) 2024-03-27 [email protected] Roll Flutter Engine from 00dab0d9d310 to b7dddee939f2 (2 revisions) (flutter/flutter#145841) 2024-03-27 [email protected] Roll Packages from ab1630b to e6b3e11 (6 revisions) (flutter/flutter#145833) 2024-03-27 [email protected] Refactor web long running tests (flutter/flutter#145776) 2024-03-27 [email protected] Roll Flutter Engine from da64c6bcbbb6 to 00dab0d9d310 (1 revision) (flutter/flutter#145830) 2024-03-27 [email protected] Roll Flutter Engine from d6c6ba5aa157 to da64c6bcbbb6 (1 revision) (flutter/flutter#145811) 2024-03-27 [email protected] Roll Flutter Engine from 064a4f5d9042 to d6c6ba5aa157 (1 revision) (flutter/flutter#145807) 2024-03-27 [email protected] Roll Flutter Engine from cad8e7a9ad70 to 064a4f5d9042 (1 revision) (flutter/flutter#145805) 2024-03-27 [email protected] Roll Flutter Engine from 441005698702 to cad8e7a9ad70 (1 revision) (flutter/flutter#145804) 2024-03-27 [email protected] Roll Flutter Engine from d872d50e53f4 to 441005698702 (1 revision) (flutter/flutter#145803) 2024-03-27 [email protected] Roll Flutter Engine from 92ebd47dd8a8 to d872d50e53f4 (6 revisions) (flutter/flutter#145801) 2024-03-27 [email protected] Update localization files. (flutter/flutter#145780) 2024-03-27 [email protected] Roll Flutter Engine from 026d8902e3b5 to 92ebd47dd8a8 (1 revision) (flutter/flutter#145788) 2024-03-26 [email protected] [web] Add BackgroundIsolateBinaryMessenger.ensureInitialized to web. (flutter/flutter#145786) 2024-03-26 49699333+dependabot[bot]@users.noreply.github.com Bump codecov/codecov-action from 4.1.0 to 4.1.1 (flutter/flutter#145787) 2024-03-26 [email protected] Roll pub packages and regenerate gradle lockfiles (flutter/flutter#145727) 2024-03-26 [email protected] Roll Flutter Engine from 5c7aea6f20fc to 026d8902e3b5 (1 revision) (flutter/flutter#145785) 2024-03-26 [email protected] Roll Flutter Engine from baede78d2352 to 5c7aea6f20fc (2 revisions) (flutter/flutter#145784) 2024-03-26 [email protected] Roll Flutter Engine from cffd1dcfe6a5 to baede78d2352 (2 revisions) (flutter/flutter#145778) 2024-03-26 [email protected] Correct typo: "Free" to "Three" in comments (flutter/flutter#145689) 2024-03-26 [email protected] Fix disabled `DropdownMenu` doesn't defer the mouse cursor (flutter/flutter#145686) 2024-03-26 [email protected] Roll Flutter Engine from b2d93a64cbc7 to cffd1dcfe6a5 (9 revisions) (flutter/flutter#145773) 2024-03-26 [email protected] Roll Packages from 28d126c to ab1630b (1 revision) (flutter/flutter#145755) 2024-03-26 [email protected] Memory leaks clean up 2 (flutter/flutter#145757) 2024-03-26 [email protected] Fix memory leak in Overlay.wrap. (flutter/flutter#145744) 2024-03-26 [email protected] Be tolerant of backticks around directory name in `pub` output. (flutter/flutter#145768) 2024-03-26 [email protected] Fix `ExpansionTile` Expanded/Collapsed announcement is interrupted by VoiceOver (flutter/flutter#143936) ...
|
I might be interested in helping with fixing the back gesture animation (if I have enough free time). At first, I just wanted to copy and customize Maybe I'm mistaken because I'm a flutter newbie, but it seems that it's currently impossible to access the required attributes from Here's what I think we need to successfully implement the back gesture animation:
Then to implement the For the Finally, for the Right now, it seems only ( |
|
For reference, I'm talking about this issue that was already discussed, but it seems not yet addressed: #141373 (review) |
|
Ah, I actually found a hacky way to fix the animation! Here my code, copied and adapted from the SDK, but doesn't to change the SDK itself: class MyPageTransition extends PageTransitionsBuilder {
@override
Widget buildTransitions<T>(
PageRoute<T> route,
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return MyPredictiveBackGestureDetector(
route: route,
builder: (BuildContext context) {
// Only do a predictive back transition when the user is performing a
// pop gesture. Otherwise, for things like button presses or other
// programmatic navigation, fall back to ZoomPageTransitionsBuilder.
if (route.popGestureInProgress) {
return MyPredictiveBackPageTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
getIsCurrent: () => route.isCurrent,
child: child,
);
}
return const ZoomPageTransitionsBuilder().buildTransitions(
route,
context,
animation,
secondaryAnimation,
child,
);
},
);
}
}
class MyPredictiveBackGestureDetector extends StatefulWidget {
const MyPredictiveBackGestureDetector({super.key,
required this.route,
required this.builder,
});
final WidgetBuilder builder;
final PredictiveBackRoute route;
@override
State<MyPredictiveBackGestureDetector> createState() =>
MyPredictiveBackGestureDetectorState();
}
class MyPredictiveBackGestureDetectorState extends State<MyPredictiveBackGestureDetector>
with WidgetsBindingObserver {
/// True when the predictive back gesture is enabled.
bool get _isEnabled {
return widget.route.isCurrent
&& widget.route.popGestureEnabled;
}
/// The back event when the gesture first started.
PredictiveBackEvent? get startBackEvent => _startBackEvent;
PredictiveBackEvent? _startBackEvent;
set startBackEvent(PredictiveBackEvent? startBackEvent) {
if (_startBackEvent != startBackEvent && mounted) {
setState(() {
_startBackEvent = startBackEvent;
});
}
}
/// The most recent back event during the gesture.
PredictiveBackEvent? get currentBackEvent => _currentBackEvent;
PredictiveBackEvent? _currentBackEvent;
set currentBackEvent(PredictiveBackEvent? currentBackEvent) {
if (_currentBackEvent != currentBackEvent && mounted) {
setState(() {
_currentBackEvent = currentBackEvent;
});
}
}
// Begin WidgetsBindingObserver.
@override
bool handleStartBackGesture(PredictiveBackEvent backEvent) {
final bool gestureInProgress = !backEvent.isButtonEvent && _isEnabled;
if (!gestureInProgress) {
return false;
}
widget.route.handleStartBackGesture(progress: 1 - backEvent.progress / 10); // SOME CHANGE HERE
startBackEvent = currentBackEvent = backEvent;
return true;
}
@override
void handleUpdateBackGestureProgress(PredictiveBackEvent backEvent) {
widget.route.handleUpdateBackGestureProgress(progress: 1 - backEvent.progress / 10); // SOME CHANGE HERE
currentBackEvent = backEvent;
}
@override
void handleCancelBackGesture() {
widget.route.handleCancelBackGesture();
startBackEvent = currentBackEvent = null;
}
@override
void handleCommitBackGesture() {
widget.route.handleCommitBackGesture();
startBackEvent = currentBackEvent = null;
}
// End WidgetsBindingObserver.
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
Widget build(BuildContext context) {
return widget.builder(context);
}
}
class MyPredictiveBackPageTransition extends StatelessWidget {
const MyPredictiveBackPageTransition({
super.key,
required this.animation,
required this.secondaryAnimation,
required this.getIsCurrent,
required this.child,
});
final Animation<double> animation;
final Animation<double> secondaryAnimation;
final ValueGetter<bool> getIsCurrent;
final Widget child;
Widget _primaryAnimatedBuilder(BuildContext context, Widget? child) { // SOME CHANGE HERE
final Size size = MediaQuery.sizeOf(context);
return Transform.translate(
offset: Offset(Tween(begin: size.width, end: 0.0).animate(animation).value, 0),
child: Transform.scale(
scale: Tween(begin: 0.5, end: 1.0).animate(animation).value,
child: Opacity(
opacity: Tween(begin: 0.5, end: 1.0).animate(animation).value,
child: child,
),
),
);
}
@override
Widget build(BuildContext context) { // SOME CHANGE HERE
return AnimatedBuilder(
animation: animation,
builder: _primaryAnimatedBuilder,
child: child,
);
}
}
|
|
I continued trying to make the animation work, and I found a few things:
Also,
Looking at this PR, it seems the code that is now used to drive android back gestures was mostly taken from the iOS back gesture code. I don't own an iOS device, but I therefore suppose they work very differently from android. Judging by the issues I had with this code on android, I infer that on iOS:
To conclude, I think the current implementation is based on the wrong assumption that iOS and Android back gestures are similar. I'm trying very hard to write some code that works around those issues to provide the best animationI can without changing the SDK itself. However, to implement the material spec fully, I think the SDK's back gesture design will need to be changed. |
|
I'm giving up making a transition that works with an unmodified SDK. The one last problem I encountered, and that I can't work around, is this line: https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/routes.dart#L541 It sets the animation curve for the commit transition, but the issue is that it is not in the This means that we can't change it or avoid it. Unfortunately, this specific curve is ill-suited for the commit transition animation. It starts extremely fast and finishes extremely slowly, when we would need the opposite to have a smooth animation meeting the material design spec. It goes so fast to a very small value that the animation goes from 1.0 to almost 0 in a small fraction of the animation duration: At most, the animation will last for 800ms ( |
|
I'm posting my playground code in case I want someday to go back to it or if it can help someone else. It solves a few issues, but it's still far from being usable. import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class MyPageTransition extends PageTransitionsBuilder {
@override
Widget buildTransitions<T>(route, context, animation, secondaryAnimation, child) {
// if (route.popGestureInProgress) {
return MyPredictiveBackGestureDetector(
route: route,
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
// } else {
// return const ZoomPageTransitionsBuilder().buildTransitions(
// route,
// context,
// animation,
// secondaryAnimation,
// child,
// );
// }
}
}
class MyPredictiveBackGestureDetector extends StatefulWidget {
const MyPredictiveBackGestureDetector({
super.key,
required this.route,
required this.animation,
required this.secondaryAnimation,
required this.child,
});
final PageRoute route;
final Animation<double> animation;
final Animation<double> secondaryAnimation;
final Widget child;
@override
State<MyPredictiveBackGestureDetector> createState() => MyPredictiveBackGestureDetectorState();
}
enum Phase {
preCommit,
postCommit,
}
class MyPredictiveBackGestureDetectorState extends State<MyPredictiveBackGestureDetector>
with WidgetsBindingObserver {
/// True when the predictive back gesture is enabled.
bool get _isEnabled {
return widget.route.isCurrent
&& widget.route.popGestureEnabled;
}
Phase? _phase;
PredictiveBackEvent? _startBackEvent;
PredictiveBackEvent? _lastBackEvent;
// Begin WidgetsBindingObserver.
@override
bool handleStartBackGesture(PredictiveBackEvent backEvent) {
if (backEvent.isButtonEvent || !_isEnabled) {
return false;
}
// Here we just call this function so that the navigator knows the gesture started but we don't actually use the progress value because the code
// in `_handleDragUpdate` is too broken to be able to use it.
widget.route.handleStartBackGesture(progress: 0.5); // abitrary number strictly between 0 and 1 so that the controller is tricked into thinking the animation is ongoing
setState(() {
_phase = Phase.preCommit;
_startBackEvent = backEvent;
_lastBackEvent = backEvent;
});
return true;
}
@override
void handleUpdateBackGestureProgress(PredictiveBackEvent backEvent) {
// Here would have been nice to update the animation progress to make the back page move in sync with the front page but the code is too broken for that
// and it would create problems when running the commit animation.
// widget.route.handleUpdateBackGestureProgress(progress: );
setState(() {
_lastBackEvent = backEvent;
});
}
@override
void handleCancelBackGesture() {
// This function is not called when the user cancels the gesture, but after the animation is complete.
// It's the system that will drive the animation backward through the BackEvent.progress value.
// Then, when the animation is complete and the front page has reached its original position, the system will call this function.
// Unfortunately, `widget.route.handleCancelBackGesture()` thinks it's his responsability to drive the animation backward, but it's not.
// I am guessing this mistake is due to the fact that the code might by inspired by the iOS implementation where I suppose the system doesn't drive the animation backward.
// So, for android, we have to trick it into thinking the animation is complete by telling it the progress is at 100% already.
widget.route.handleUpdateBackGestureProgress(progress: 1.0);
widget.route.handleCancelBackGesture();
}
@override
void handleCommitBackGesture() {
// This function is called when the user releases the gesture and the system decides to commit the back gesture.
// The commit animation is not just a continuation of the pre-commit animation that would simulate the user's finger finishing crossing the screen.
// Actually, in the Clock app, the commit animation goes back in the direction of the origin edge and overshoots it.
// Unfortunatly, the code run by `widget.route.handleCommitBackGesture()` is probably inspired the iOS implementation where I suppose the animation is supposed
// to continue in the same direction as the pre-commit animation.
// So, for android, we want a new animation to start and so we need its controller to start at 0.
// In order to do that, we have to trick `_handleDragEnd` into thinking we are at 0% of the animation.
// However, to complete the progress of the commit animation `_handleDragEnd` sets up the controller in a backward motion, so instead of telling him that the progress
// is now at 0%, we have to tell him that the progress is at 100%.
// I don't know why `_handleDragEnd` mixes up frontward and backward motions but I guess it comes from the original iOS implementation.
widget.route.handleUpdateBackGestureProgress(progress: 0.5);
// This only works because we decided to not use the progress value to animate the back page.
// If we tried to animate the back page in sync with the front page, we would either have a jump in the backpage animation or an insanly fast front page commit animation.
// It is not resolvable without a complete rewrite of the code of `_handleDragEnd`.
widget.route.handleCommitBackGesture();
setState(() {
_phase = Phase.postCommit;
});
}
// End WidgetsBindingObserver.
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
Widget build(BuildContext context) {
final Size size = MediaQuery.sizeOf(context);
const slideRatio = 0.06;
const scaleRatio = slideRatio * 3;
const commitOvershootFactor = 1.2;
if (_phase != null) { // animation for the front page
final progress = _lastBackEvent!.progress;
final yOffset = (_lastBackEvent!.touchOffset!.dy - _startBackEvent!.touchOffset!.dy);
final fromLeftEdge = _lastBackEvent!.swipeEdge == SwipeEdge.left;
switch (_phase!) {
case Phase.preCommit:
return Transform.translate(
offset: Offset(
progress * slideRatio * size.width * (fromLeftEdge ? 1 : -1),
progress * slideRatio * yOffset
),
child: Transform.scale(
scale: 1 - progress * scaleRatio,
child: widget.child
),
);
case Phase.postCommit:
final animationValue = sqrt(sqrt(widget.animation.value)); // try to correct a little the `fastLinearToSlowEaseIn` curve in `_handleDragEnd`
return Transform.translate(
offset: Offset(
progress * slideRatio * size.width * (fromLeftEdge ? 1 : -1) * animationValue * animationValue + (1 - animationValue) * commitOvershootFactor * size.width,
progress * slideRatio * yOffset * animationValue,
),
child: Transform.scale(
scale: 1 - progress * scaleRatio, // stays the same
child: Opacity(
opacity: animationValue,
child: widget.child
)
),
);
}
} else { // animation for the back page
// Unfortunatly, the code in `_handleDragEnd` is too broken to be able to use the progress value to animate the back page in sync with the front page.
return widget.child;
// final progress = Tween(begin: 0.0, end: 1.0).animate(widget.secondaryAnimation).value;
// return Transform.translate(
// offset: Offset(-slideRatio*progress*size.width, 0.0),
// child: Transform.scale(
// scale: 1-progress*slideRatio,
// child: Opacity(
// opacity: 1-progress*0.5,
// child: widget.child
// )
// ),
// );
}
}
} |
This comment was marked as spam.
This comment was marked as spam.
This comment was marked as spam.
This comment was marked as spam.
|
@PaulGrandperrin Thanks for doing all this investigation here. Are you interested in opening a PR to improve the transition in the framework? I noticed some of these problems and had intended to do a round of improvements myself (old draft PR: #146789). |
|
Hi @justinmc ! I can't say how much and at what pace I'll work on it, I can't really afford to spend too much time on this, but it's a big itch I'd like to scratch. I really like the concept of predictive back gestures as it makes it worth it to really create beautiful transitions that users can then explore at their own pace. So it was so frustrating to encounter a blocker that couldn't be worked around! Anyway, right now, in my PR, there's the strict minimum to fix the SDK API and provide a marginally better transition animation. I took some time to investigate how the classic back button events are dispatched, and I was thinking that in theory, the back gesture events should follow the same path. What do you think? Since the two top-most routes should be animated by those gesture events, I thought it would make the architecture cleaner: events would go top to bottom in the router(s) and navigator(s) in similar manner to the back button events. Anyway, that was what I thought before spending some time in the 6000+ lines of What's your opinion? (Maybe you can answer on the PR) |
|
Adding a quick post about how to enable predictive back as of now (2024.11.15).
return MaterialApp(
theme: ThemeData(
pageTransitionsTheme: PageTransitionsTheme(
builders: {
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(),
},
),
...
),
);
That's it. You should see predictive back occur both when swiping back to exit the app and when swiping back between routes within the app. |
Hi Justin, Thanks! |
|
It should definitely work with dialogs, if not it's a bug. Can you file an issue with a reproduction? |
I've had this same issue. I'm pretty sure it's just that I need to implement a lot more of the PopScope stuff. |
It appears as though that never happened and it is still one. |


This pull request introduces Android's predictive back feature for routes. By leveraging the OnBackAnimationCallback API, this implementation gathers real-time information about back gestures, enabling the framework to dynamically update route transition animations. The callbacks for this feature are forwarded to the framework as detailed in flutter/engine#49093 PR.
Here's a brief video demo showcasing the Back Preview animation.
predictive_back.mp4
Code sample
Resources
Closes #131961
Depends on engine PR flutter/engine#49093
Pre-launch Checklist
///).If you need help, consider asking for advice on the #hackers-new channel on Discord.