Is there a way to set an animation between two specific pages? I'm using onGenerateRoute to change animations between pages, but it can only filter through the page it's being navigated to.
Example: I have PageA, PageB and PageC, i can change the navigation animation when i'm navigating to PageC, but it doesn't matter if i'm navigating from PageA or PageB, the animation will always be the same.
What i'm asking for is if there is a way to make a custom animation specifically when i'm navigating between PageA and PageC and PageB and PageC.
Create Animated Navigation like this:
import 'package:flutter/material.dart';
void main() {
runApp(
const MaterialApp(
home: Page1(),
),
);
}
class Page1 extends StatelessWidget {
const Page1({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.of(context).push(_createRoute());
},
child: const Text('Go!'),
),
),
);
}
}
Route _createRoute() {
return PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => const Page2(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
const begin = Offset(0.0, 1.0);
const end = Offset.zero;
const curve = Curves.ease;
var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
return SlideTransition(
position: animation.drive(tween),
child: child,
);
},
);
}
class Page2 extends StatelessWidget {
const Page2({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: const Center(
child: Text('Page 2'),
),
);
}
}
I'm setting my routes and transition animations through onGenerateRoute which is working great except I want differents transition animations depending on a condition (on choosen path for example) :
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
onGenerateRoute: (RouteSettings settings) {
if (settings.name == '/') {
return PageRouteBuilder<dynamic>(
pageBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) =>
Page1(),
transitionsBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return SlideTransition(
position: Tween<Offset>(
begin: Offset.zero,
end: const Offset(-1.0, 0.0),
).animate(secondaryAnimation), // Here I want to have differents transitions
child: child,
);
},
);
} else if (settings.name == '/second') {
return PageRouteBuilder<dynamic>(
pageBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) =>
Page2(),
transitionsBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0), end: Offset.zero)
.animate(animation),
child: SlideTransition(
position: Tween<Offset>(
begin: Offset.zero,
end: const Offset(0.0, 1.0),
).animate(secondaryAnimation),
child: child,
));
},
);
} else if (settings.name == '/third') {
return PageRouteBuilder<dynamic>(
pageBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) =>
Page3(),
transitionsBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.0, 1.0), end: Offset.zero)
.animate(animation),
child: SlideTransition(
position: Tween<Offset>(
begin: Offset.zero,
end: const Offset(0.0, 1.0),
).animate(secondaryAnimation),
child: child,
));
},
);
} else {
return null;
}
},
);
}
}
class Page1 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Page 1"), backgroundColor: Colors.blue),
backgroundColor: Colors.blue,
body: Column(
children: [
Center(
child: RaisedButton(
onPressed: () => Navigator.pushNamed(context, '/second'),
child: Text("Go to Page 2 with exit transition slide left"),
),
),
Center(
child: RaisedButton(
onPressed: () => Navigator.pushNamed(context, '/third'),
child: Text("Go to Page 3 with transition slide top"),
),
),
],
),
);
}
}
class Page2 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Page 2"), backgroundColor: Colors.green),
backgroundColor: Colors.green,
body: Center(
child: RaisedButton(
onPressed: () => Navigator.of(context).pop(),
child: Text("Go back to page 1"))),
);
}
}
class Page3 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Page 3"), backgroundColor: Colors.yellow),
backgroundColor: Colors.yellow,
body: Center(
child: RaisedButton(
onPressed: () => Navigator.of(context).pop(),
child: Text("Go back to page 1"))),
);
}
}
See codePen here
As the exit transition animation (via secondaryAnimation) is set before the screen is displayed, I can't change the animation dynamically depending on the button clicked. How can I achieve that ?
(RouteSettings settings) {
final transition = settings.arguments as String;
if (settings.name == '/') {
return PageRouteBuilder<dynamic>(
pageBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) =>
Page1(),
transitionsBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
switch (transition) {
case 'firstTransition':
secondaryAnimation = //first animation
break;
case 'secondTransition':
secondaryAnimation = //second animation
break;
}
return SlideTransition(
position: Tween<Offset>(
begin: Offset.zero,
end: const Offset(-1.0, 0.0),
).animate(secondaryAnimation), // Here I want to have differents transitions
child: child,
);
},
);
Is there any way to animate rotation of a widget? I tried RotatedBox but it didn't work.
Use AnimatedBuilder
AnimatedBuilder(
animation: _animation, // pass AnimationController to it
child: YourContainer(),
builder: (_, child) {
return Transform.rotate(
angle: _animation.value * play_around_with_values,
child: child,
);
},
)
Screenshot:
Full code:
class _MainPageState extends State<MainPage> with SingleTickerProviderStateMixin {
AnimationController _controller;
#override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: Duration(seconds: 2))..repeat();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: AnimatedBuilder(
animation: _controller,
builder: (_, child) {
return Transform.rotate(
angle: _controller.value * 2 * math.pi,
child: child,
);
},
child: FlutterLogo(size: 200),
),
),
);
}
}
On Android API we can use
overridePendingTransition(int enterAnim, int exitAnim)
to define the enter and exit transitions.
How to do it in Flutter?
I have implemented this code
class SlideLeftRoute extends PageRouteBuilder {
final Widget enterWidget;
SlideLeftRoute({this.enterWidget})
: super(
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return enterWidget;
},
transitionsBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
return SlideTransition(
position: new Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(animation),
child: child
);
},
);
}
but it only defines the enter transition. How can i define de exit transition?
UPDATE
Imagine that i have two screens (Screen1 and Screen2), when i execute
Navigator.push(
context, SlideLeftRoute(enterWidget: Screen2()));
i'd like to apply an animation to both Screen1 and Screen2 and not only to Screen2
The correct way of achieving this is to use the secondaryAnimation parameter that is given in the transitionBuilder of a PageRouteBuilder object.
Here you can read more about the secondaryAnimation parameter in the documentation in the flutter/lib/src/widgets/routes.dart file in the flutter sdk:
///
/// When the [Navigator] pushes a route on the top of its stack, the
/// [secondaryAnimation] can be used to define how the route that was on
/// the top of the stack leaves the screen. Similarly when the topmost route
/// is popped, the secondaryAnimation can be used to define how the route
/// below it reappears on the screen. When the Navigator pushes a new route
/// on the top of its stack, the old topmost route's secondaryAnimation
/// runs from 0.0 to 1.0. When the Navigator pops the topmost route, the
/// secondaryAnimation for the route below it runs from 1.0 to 0.0.
///
/// The example below adds a transition that's driven by the
/// [secondaryAnimation]. When this route disappears because a new route has
/// been pushed on top of it, it translates in the opposite direction of
/// the new route. Likewise when the route is exposed because the topmost
/// route has been popped off.
///
/// ```dart
/// transitionsBuilder: (
/// BuildContext context,
/// Animation<double> animation,
/// Animation<double> secondaryAnimation,
/// Widget child,
/// ) {
/// return SlideTransition(
/// position: AlignmentTween(
/// begin: const Offset(0.0, 1.0),
/// end: Offset.zero,
/// ).animate(animation),
/// child: SlideTransition(
/// position: TweenOffset(
/// begin: Offset.zero,
/// end: const Offset(0.0, 1.0),
/// ).animate(secondaryAnimation),
/// child: child,
/// ),
/// );
/// }
/// ```
This is a working example using the secondaryAnimation parameter:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
onGenerateRoute: (RouteSettings settings) {
if (settings.name == '/') {
return PageRouteBuilder<dynamic>(
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) => Page1(),
transitionsBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
final Tween<Offset> offsetTween = Tween<Offset>(begin: Offset(0.0, 0.0), end: Offset(-1.0, 0.0));
final Animation<Offset> slideOutLeftAnimation = offsetTween.animate(secondaryAnimation);
return SlideTransition(position: slideOutLeftAnimation, child: child);
},
);
} else {
// handle other routes here
return null;
}
},
);
}
}
class Page1 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Page 1")),
backgroundColor: Colors.blue,
body: Center(
child: RaisedButton(
onPressed: () => Navigator.push(
context,
PageRouteBuilder<dynamic>(
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) => Page2(),
transitionsBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
final Tween<Offset> offsetTween = Tween<Offset>(begin: Offset(1.0, 0.0), end: Offset(0.0, 0.0));
final Animation<Offset> slideInFromTheRightAnimation = offsetTween.animate(animation);
return SlideTransition(position: slideInFromTheRightAnimation, child: child);
},
),
),
child: Text("Go to Page 2"),
),
),
);
}
}
class Page2 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Page 2"), backgroundColor: Colors.green),
backgroundColor: Colors.green,
body: Center(child: RaisedButton(onPressed: () => Navigator.pop(context), child: Text("Back to Page 1"))),
);
}
}
Result:
Screenshot (Null Safe):
I used a different way, but similar logic provided by diegodeveloper
Full code:
void main() => runApp(MaterialApp(home: Page1()));
class Page1 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey,
appBar: AppBar(title: Text('Page 1')),
body: Center(
child: ElevatedButton(
onPressed: () => Navigator.push(
context,
MyCustomPageRoute(
parent: this,
builder: (context) => Page2(),
),
),
child: Text('2nd Page'),
),
),
);
}
}
class Page2 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.blueGrey,
appBar: AppBar(title: Text('Page 2')),
body: Center(
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
child: Text('Back'),
),
),
);
}
}
class MyCustomPageRoute<T> extends MaterialPageRoute<T> {
final Widget parent;
MyCustomPageRoute({
required this.parent,
required WidgetBuilder builder,
RouteSettings? settings,
}) : super(builder: builder, settings: settings);
#override
Widget buildTransitions(_, Animation<double> animation, __, Widget child) {
var anim1 = Tween<Offset>(begin: Offset.zero, end: Offset(-1.0, 0.0)).animate(animation);
var anim2 = Tween<Offset>(begin: Offset(1.0, 0.0), end: Offset.zero).animate(animation);
return Stack(
children: <Widget>[
SlideTransition(position: anim1, child: parent),
SlideTransition(position: anim2, child: child),
],
);
}
}
Good question , the PageRouteBuilder use an AnimationController by default to handle the animation transition so, when you dismiss your view, it just call 'reverse' method from the animationController and you will see the same animation you are using but in reverse.
In case you want to change the animation when you dismiss your view you can do it checking the status of the current animation and compare with AnimationStatus.reverse
This is your code with a Fade animation when it's in reverse.
class SlideLeftRoute extends PageRouteBuilder {
final Widget enterWidget;
SlideLeftRoute({this.enterWidget})
: super(
pageBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
return enterWidget;
},
transitionsBuilder: (BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child) {
if (animation.status == AnimationStatus.reverse) {
//do your dismiss animation here
return FadeTransition(
opacity: animation,
child: child,
);
} else {
return SlideTransition(
position: new Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(animation),
child: child);
}
},
);
}
WORKAROUND
class SlideLeftRoute extends PageRouteBuilder {
final Widget enterWidget;
final Widget oldWidget;
SlideLeftRoute({this.enterWidget, this.oldWidget})
: super(
transitionDuration: Duration(milliseconds: 600),
pageBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
return enterWidget;
},
transitionsBuilder: (BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child) {
return Stack(
children: <Widget>[
SlideTransition(
position: new Tween<Offset>(
begin: const Offset(0.0, 0.0),
end: const Offset(-1.0, 0.0),
).animate(animation),
child: oldWidget),
SlideTransition(
position: new Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(animation),
child: enterWidget)
],
);
});
}
Usage:
Navigator.of(context)
.push(SlideLeftRoute(enterWidget: Page2(), oldWidget: this));
#janosch's answer worked the best for me (so be sure to upvote him if this works for you), but #bihire boris's point about not being able to have more than 2 page routes in a navigation stack was true.
What worked for me was this:
class SlidingPageRouteBuilder extends PageRouteBuilder {
SlidingPageRouteBuilder({
required RoutePageBuilder pageBuilder,
}) : super(
transitionDuration: const Duration(milliseconds: 300),
reverseTransitionDuration: const Duration(milliseconds: 300),
pageBuilder: pageBuilder,
transitionsBuilder: (BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child) {
final pushingNext =
secondaryAnimation.status == AnimationStatus.forward;
final poppingNext =
secondaryAnimation.status == AnimationStatus.reverse;
final pushingOrPoppingNext = pushingNext || poppingNext;
late final Tween<Offset> offsetTween = pushingOrPoppingNext
? Tween<Offset>(
begin: const Offset(0.0, 0.0), end: const Offset(-1.0, 0.0))
: Tween<Offset>(
begin: const Offset(1.0, 0.0), end: const Offset(0.0, 0.0));
late final Animation<Offset> slidingAnimation = pushingOrPoppingNext
? offsetTween.animate(secondaryAnimation)
: offsetTween.animate(animation);
return SlideTransition(position: slidingAnimation, child: child);
},
);
}
And the implementation:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: Routes.first,
onGenerateRoute: Routes.generateRoutes,
},
);
}
}
class Routes {
static const String first = '/first';
static const String second = '/second';
static Route<dynamic>? generateRoutes(RouteSettings settings) {
switch (settings.name) {
case first:
return SlidingPageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const FirstPage(),
);
case second:
return SlidingPageRouteBuilder(
pageBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) =>
const SecondPage(),
);
default:
return null;
}
}
}
Result:
The one downside to this method (and all other methods I've seen) is that the user cannot drag their finger along the left edge of the screen towards the right to pop, in the way that a MaterialPageRoute lets you.
There is another way to do it. The problem of initState() getting called in oldWidget won't be there anymore.
void main() => runApp(MaterialApp(theme: ThemeData.dark(), home: HomePage()));
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Page 1")),
body: RaisedButton(
child: Text("Next"),
onPressed: () {
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (c, a1, a2) => Page2(),
transitionsBuilder: (context, anim1, anim2, child) {
return SlideTransition(
position: Tween<Offset>(end: Offset(0, 0), begin: Offset(1, 0)).animate(anim1),
child: Page2(),
);
},
),
);
},
),
);
}
}
class Page2 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Page 2")),
body: RaisedButton(
child: Text("Back"),
onPressed: () => Navigator.pop(context),
),
);
}
}