I tried Hero widget and page route to switch between pages like this
gif.
I had try with custom page route, use Stack widget to wrap two pages, and animated in buildTransitions, but it is hard to calculate scale alignment and hero widget didn't remove from the previous page.
class CustomPageRoute extends MaterialPageRoute {
final Widget parentWidget;
final Alignment parentAlignment;
final Tween<double> childScaleTween;
final double childWidth;
CustomPageRoute(
{required this.parentWidget,
required this.parentAlignment,
required this.childScaleTween,
required this.childWidth,
required WidgetBuilder builder,
RouteSettings? settings})
: super(builder: builder, settings: settings);
#override
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
return builder(context);
}
#override
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
Curve animatedCurves = Curves.linear;
var scale = MediaQuery.of(context).size.width / childWidth;
var scaleAnim = Tween(begin: 1.0, end: scale)
.animate(CurvedAnimation(parent: animation, curve: animatedCurves));
var fadeAnim = Tween(begin: 1.0, end: 0.0)
.animate(CurvedAnimation(parent: animation, curve: animatedCurves));
var childScaleAnim = childScaleTween
.animate(CurvedAnimation(parent: animation, curve: animatedCurves));
var childFadeAnim = Tween(begin: 0.0, end: 1.0)
.animate(CurvedAnimation(parent: animation, curve: animatedCurves));
return Stack(children: [
Container(
color: Colors.white,
),
ScaleTransition(
scale: scaleAnim,
alignment: parentAlignment,
child: FadeTransition(opacity: fadeAnim, child: parentWidget)),
ScaleTransition(
scale: childScaleAnim,
alignment: parentAlignment,
child: FadeTransition(opacity: childFadeAnim, child: child)),
]);
}
}
Are there any other ideas to provide?
try animations, Container transform:
_OpenContainerWrapper(
transitionType: _transitionType,
closedBuilder: (BuildContext _, VoidCallback openContainer) {
return _ExampleCard(openContainer: openContainer);
},
onClosed: _showMarkedAsDoneSnackbar,
),
class _OpenContainerWrapper extends StatelessWidget {
const _OpenContainerWrapper({
required this.closedBuilder,
required this.transitionType,
required this.onClosed,
});
final CloseContainerBuilder closedBuilder;
final ContainerTransitionType transitionType;
final ClosedCallback<bool?> onClosed;
#override
Widget build(BuildContext context) {
return OpenContainer<bool>(
transitionType: transitionType,
openBuilder: (BuildContext context, VoidCallback _) {
return const _DetailsPage();
},
onClosed: onClosed,
tappable: false,
closedBuilder: closedBuilder,
);
}
}
Related
The bounty expires in 5 days. Answers to this question are eligible for a +100 reputation bounty.
Chris wants to draw more attention to this question.
I have a CustomPageRoute for a fade animation when pushing to another screen. That is working as expected.
However I would love to have a swipe back animation and can not make it work. I tried couple of different things, the most promising was this answer but that ist still sliding in/out the page.
For animation I only want the a fadeAnimation, both for pushing and popping.
This is my code for the FadeTransition:
class FadePageTransition extends Page {
final Widget page;
const FadePageTransition({
required this.page,
LocalKey? key,
String? restorationId,
}) : super(
key: key,
restorationId: restorationId,
);
#override
Route createRoute(BuildContext context) {
return FadeRoute(
child: page,
routeSettings: this,
);
}
}
class FadeRoute<T> extends PageRoute<T> {
final Widget child;
final RouteSettings routeSettings;
FadeRoute({
required this.child,
required this.routeSettings,
});
#override
RouteSettings get settings => routeSettings;
#override
Color? get barrierColor => Palette.black;
#override
String? get barrierLabel => null;
#override
Widget buildPage(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
return FadeTransition(
opacity: animation,
child: child,
);
}
#override
bool get maintainState => true;
#override
Duration get transitionDuration => const Duration(
milliseconds: transitionDurationInMS,
);
}
Let me know if you need any more info.
This is my approach for fade in and slide transitions if you can figure out from context if you are adding or removing from route you put condition and that should work.
Future nextScreenSlideIn(BuildContext context, page) {
PageRouteBuilder builder = PageRouteBuilder(
pageBuilder: (_, Animation animation, Animation secondaryAnimation) {
return page;
},
transitionsBuilder: (
_,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1, 0),
end: Offset.zero,
).animate(animation),
child: child,
);
},
transitionDuration: const Duration(milliseconds: 200));
return Navigator.push(context, builder);
}
Future nextScreenFadeIn(BuildContext context, page) {
PageRouteBuilder builder = PageRouteBuilder(
pageBuilder:
(_, Animation<double> animation, Animation secondaryAnimation) {
return page;
},
transitionsBuilder: (
_,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return FadeTransition(
opacity: Tween<double>(begin: 0, end: 1).animate(animation),
child: child,
);
},
transitionDuration: const Duration(milliseconds: 200));
return Navigator.push(context, builder);
}
or
You can try following package: https://pub.dev/packages/page_transition#do-you-want-use-theme-transitions-
it has also mentioned similar thing.
I want to animate page transition conditionally.
Those conditions are forward vs backward and backward(touch close button) vs backward(scroll down gesture)
I used CustomPageRouteBuilder.
So I achieved different transition between forward vs backward.
Because there is AnimationStatus.forward property.
Below is CustomPageRouteBuilder.
import 'package:flutter/material.dart';
class CustomPageRouteBuilder extends PageRouteBuilder {
final Widget child;
CustomPageRouteBuilder({
required this.child,
}) : super(
pageBuilder: (context, animation, secondaryAnimation) => child,
);
#override
Widget buildTransitions(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
if (animation.status == AnimationStatus.forward) {
return child;
} else {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.5),
end: Offset.zero,
).animate(animation),
child: FadeTransition(
opacity: animation,
child: child,
),
);
}
}
}
But how can achieve different page transition between backward(touch close button) vs backward(scroll down gesture)
Is there anyway to distinguish these?
I am trying to achieve a nice simple fade animation from one navigation route to another using PageRouteBuilder. I want the current route to fade out completely, then after the old route is gone, the new route should fade in.
So far in my PageRouteBuilder class, I can fade the new route in from 0 to 1, but I want the old route to fade out fully first, then after the old route has faded out for the new route to fade in. So far in my current code, the old route disappears suddenly once the new route fading in has finished.
I also want to emphasise that I do not want them to fade out/in at the same time, but for the fade out of the old route then fade in for the new route to happen in sequence.
import 'package:flutter/material.dart';
class FadePageTransition extends PageRouteBuilder {
final Widget child;
FadePageTransition({
required this.child,
}) : super(
transitionDuration: const Duration(milliseconds: 600),
pageBuilder: (context, animation, secondaryAnimation) => child,
);
#override
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) => FadeTransition(
opacity: animation,
child: child,
);
}
I know that the secondaryAnimation property controls the animation for how the old route leaves, doesn't it? but i'm just not sure how that would work in this context.
Use 2 FadeTransition in transitionBuilder: is the solution to your problem.
If that explanation is kinda hard to understand, this is how you use it.
transitionsBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return FadeTransition(
opacity: animation,
child: FadeTransition(
opacity: secondaryAnimation,
child: child,
),
);
}
That's it!
You have to make special curves to get this to work.
class DelayedCurve extends Curve {
const DelayedCurve() : super();
#override
double transformInternal(double t) {
if (t < 0.5) {
return 0.0;
} else {
return (t - 0.5) * 2.0;
}
}
}
class CurveDelayed extends Curve {
const CurveDelayed() : super();
#override
double transformInternal(double t) {
if (t > 0.5) {
return 1.0;
} else {
return t * 2;
}
}
}
and you have to drive them correctly.
class FadeFirstTransition extends PageRouteBuilder {
final Widget child;
FadeFirstTransition({
required this.child,
}) : super(
transitionDuration: const Duration(milliseconds: 3000),
reverseTransitionDuration: const Duration(milliseconds: 3000),
pageBuilder: (context, animation, secondaryAnimation) => child,
);
#override
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) =>
FadeTransition(
opacity: animation.drive(Tween<double>(begin: 0, end: 1)
.chain(CurveTween(curve: const DelayedCurve()))),
child: FadeTransition(
opacity: secondaryAnimation.drive(Tween<double>(begin: 1, end: 0)
.chain(CurveTween(curve: const CurveDelayed()))),
child: child,
));
}
Here i wrote a simple code to show a container with SlideTransition animation. in this code i try to slide the container from 0.9 of screen from bottom but in this code animation started from zero, that means i can't limit this slide animation
for example you suppose i have a Container with 400 height and i want to slide the container from 300 to 400. but i can't
class ChannelDetailRouter extends PageRoute<void> {
final WidgetBuilder builder;
ChannelDetailRouter({required this.builder, RouteSettings? settings}) : super(settings: settings);
#override
bool get opaque => false;
#override
Color? get barrierColor => null;
#override
String? get barrierLabel => null;
#override
bool get maintainState => true;
#override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return GestureDetector(
onTap:()=>Navigator.of(context).pop(),
child: Container(
color: Colors.black.withOpacity(0.5),
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.9),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeIn,
)),
child: builder(context),
),
),
);
}
#override
Duration get transitionDuration => Duration(milliseconds: 350);
}
you were very close to the answer just change
begin: const Offset(0, 0.9),
to
begin: const Offset(0, 0.1),
I was searching for a way to combine repetitive and not repetitive animations in Flutter. For example, start opening animation (not repetitive) and then show some repetitive animation, like bouncing animation. Currently, I've used 2 animated controllers and 2 animatedBuilders above one widget. Here is my sample code:
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _outerAnimationController, //starting animation, not repetitive
builder: (context, _) {
return AnimatedBuilder(
animation: _innerCurvedAnimation, //repetitive animation, bouncing
builder: (context, _) {
return CustomPaint(
size: Size(MediaQuery.of(context).size.width,
MediaQuery.of(context).size.height),
painter: ShowCasePainter(
centerPosition: Offset(
MediaQuery.of(context).size.width / 2,
MediaQuery.of(context).size.height / 2),
innerCircleRadius: widget.innerCircleRadius +
(_innerCurvedAnimation.value * PaddingSmall), //repetitive animation value, bouncing
outerCircleRadius: _outerAnimationTween.value, //starting animation value, not repetitive
),
);
});
});
}
Is it a good practice on using multiple controllers in this way? How to influence animatedBuilder from two controllers representing different animations?
Thanks for your help!
With the help of #pskink I've finally found solution. No need in use multiple AnimatedBuilders here, Listenable.merge with multiple AnimationControllers will be enought. Merging possible because AnimationControllers extends from Listenable class. Here is correct code:
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: Listenable.merge(
[_innerAnimationController, _outerAnimationController]),
builder: (context, _) {
return CustomPaint(
size: Size(MediaQuery.of(context).size.width,
MediaQuery.of(context).size.height),
painter: ShowCasePainter(
centerPosition: Offset(MediaQuery.of(context).size.width / 2,
MediaQuery.of(context).size.height / 2),
innerCircleRadius: widget.innerCircleRadius +
(_innerCurvedAnimation.value * PaddingSmall),
outerCircleRadius: _outerAnimationTween.value,
),
);
});
}
This solution will be ok with any widget, but with CustomPainter there is a better solution. In case of Custom painter, animationControllers can be passed throught constructor into CustomPainter and inside of this class should be merged.
Parent build method:
#override
Widget build(BuildContext context) {
return CustomPaint(
willChange: true,
size: Size(MediaQuery.of(context).size.width,
MediaQuery.of(context).size.height),
painter: ShowCasePainter(
centerPosition: Offset(MediaQuery.of(context).size.width / 2,
MediaQuery.of(context).size.height / 2),
innerCircleRadius: widget.innerCircleRadius,
outerCircleRadius: widget.outerCircleRadius,
innerAnimationController: _innerAnimationController,
outerAnimationController: _outerAnimationController,
),
);
}
As you see, I passed only immutable values, instead of using values from controllers/animations.
And here is CustomPainter constructor:
class ShowCasePainter extends CustomPainter {
final Offset centerPosition;
final double innerCircleRadius;
final double outerCircleRadius;
final Color backgroundColor;
final Color ringColor;
final Animation<double> innerAnimationController;
final Animation<double> outerAnimationController;
Animation<double> _innerCurvedAnimation;
Animation<double> _outerAnimationTween;
Animation<Color> _backgroudColorTween;
ShowCasePainter(
{this.innerAnimationController,
this.outerAnimationController,
this.centerPosition,
this.innerCircleRadius = 32.0,
this.outerCircleRadius = 128.0,
this.backgroundColor,
this.ringColor})
: super(
repaint: Listenable.merge(
[innerAnimationController, outerAnimationController])) {
_innerCurvedAnimation =
CurvedAnimation(parent: innerAnimationController, curve: Curves.easeIn);
_outerAnimationTween =
Tween(begin: innerCircleRadius, end: outerCircleRadius)
.animate(outerAnimationController);
_backgroudColorTween = ColorTween(
begin: Colors.transparent,
end: backgroundColor ?? Colors.black.withOpacity(0.2))
.animate(outerAnimationController);
}
Now animations will work correct.