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.
Related
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,
));
}
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,
);
}
}
I need to make the modal route animation come from the bottom when clicked, however the only animation that I managed to do is the fade away and the spin animation.
It is an overlay that is called when the user clicks a button to explain it.
Here is the build page
#override
Widget buildPage(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
return Material(
type: MaterialType.transparency,
// make sure that the overlay content is not cut off
child: SafeArea(
child: _buildOverlayContent(context),
),
);
}
and here is the build transitions:
#override
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
// You can add your own animations for the overlay content
return FadeTransition(
opacity: animation,
child: ScaleTransition(
scale: animation,
child: child,
),
);
}
I have tried to use slide transitions from several ways but it doesnt seem to work any time I try. It either leads to an error or simply doesnt work.
(One of my tries)
Animation<Offset> animated() {
Animation<Offset> anime;
return anime;
}
#override
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
//You can add your own animations for the overlay content
return SlideTransition(
position: animated(),
child: child,
);
}
What should I do?
You can use the page_transition package:
https://pub.dev/packages/page_transition
You can use the bottomToTop transition to achieve this.
Navigator.push(
context,
PageTransition(
type: PageTransitionType.bottomToTop,
child: DetailScreen()
)
);
This should achieve what you're looking for:
#override
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
return SlideTransition(
position: Tween(
begin: const Offset(0.0, 1.0),
end: Offset.zero,
).animate(animation),
child: child,
);
}
I would like to rotate an Image indefinitely.
This container is one of the widget within the stack and would like this to be rotating continuously non stop.
final AnimationController animation = AnimationController(
duration: const Duration(milliseconds: 1800),
vsync: const NonStopVSync(),
)..repeat();
final Tween tween = Tween(begin: 0.0, end: math.pi);
var square = Container(
width: 100,
height: 100,
transform: Matrix4.identity(),
color: Colors.amber,
);
...
class Foo extends State<Bar> {
...
animation.addListener((){
square.transform = Matrix4.rotationZ(tween.evaluate(animation));
});
Widget build(BuildContext context) {
return Stack(
children: [
...
Center(
child: square
)
]
)
}
}
and I get this error: 'transform' can't be used as a setter because it's final. (assignment_to_final at [digital_clock] lib/digital_clock.dart:139)
How would I do what I'm trying to do?
Try something like this:
class InfiniteAnimation extends StatefulWidget {
final Widget child;
final int durationInSeconds;
InfiniteAnimation({#required this.child, this.durationInSeconds = 2,});
#override
_InfiniteAnimationState createState() => _InfiniteAnimationState();
}
class _InfiniteAnimationState extends State<InfiniteAnimation>
with SingleTickerProviderStateMixin {
AnimationController animationController;
Animation<double> animation;
#override
void initState() {
super.initState();
animationController = AnimationController(
vsync: this,
duration: Duration(seconds: widget.durationInSeconds),
);
animation = Tween<double>(
begin: 0,
end: 12.5664, // 2Radians (360 degrees)
).animate(animationController);
animationController.forward();
animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
animationController.repeat();
}
});
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animationController,
builder: (context, child) => Transform.rotate(
angle: animation.value,
child: widget.child,
),
);
}
#override
void dispose() {
animationController?.dispose();
super.dispose();
}
}
You basically need to create a StatefulWidget that mixes in (with keyword) the SingleTickerProviderStateMixin, provide an AnimationController, start the animation, then when the animation completes, repeat.
The AnimationBuilder is a better way of telling the widget to update on every frame without having to listen to the animationController and call setState explicitly.
You can use it like this:
InfiniteAnimation(
durationInSeconds: 2, // this is the default value
child: Icon(
Icons.expand_more,
size: 50.0,
color: Colors.white,
),
)
in this simple sample code i want to have fadeIn and fadeOut animation together, but in this code fadeIn work only and reverse not work, how can i have both of them together?
import 'package:flutter/material.dart';
void main()=>runApp(MaterialApp(home: FadeTransitionSample(),));
class FadeTransitionSample extends StatefulWidget {
#override
State<StatefulWidget> createState() => _Fade();
}
class _Fade extends State<FadeTransitionSample> with TickerProviderStateMixin {
AnimationController animation;
Animation<double> _fadeInFadeOut;
#override
void initState() {
super.initState();
animation = AnimationController(vsync: this, duration: Duration(seconds: 3),);
_fadeInFadeOut = Tween<double>(begin: 0.0, end: 0.1).animate(animation);
animation.addListener((){
if(animation.isCompleted){
animation.reverse();
}else{
animation.forward();
}
});
animation.repeat();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Center(
child: FadeTransition(
opacity: animation,
child: Container(
color: Colors.green,
width: 100,
height: 100,
),
),
),
),
);
}
}
Alright, I am assuming, you are looking to get a FadeIn & FadeOut animation on your container.
There are a few things you need to change.
The FadeTransition class should not take the animation for opacity, rather it should be the _fadeInFadeOut variable
The animation starts, when you call the animation.forward() rather than animation.repeat() (Since in your case, you also need to reverse the animation, always start with the animation.forward() call).
Make sure to use the addStatusListener() method instead of addListener() since you get much better control over your states.
So, all of this put together, below is the working code to make your animation work.
import 'package:flutter/material.dart';
void main()=>runApp(MaterialApp(home: FadeTransitionSample(),));
class FadeTransitionSample extends StatefulWidget {
#override
State<StatefulWidget> createState() => _Fade();
}
class _Fade extends State<FadeTransitionSample> with TickerProviderStateMixin {
AnimationController animation;
Animation<double> _fadeInFadeOut;
#override
void initState() {
super.initState();
animation = AnimationController(vsync: this, duration: Duration(seconds: 3),);
_fadeInFadeOut = Tween<double>(begin: 0.0, end: 0.5).animate(animation);
animation.addStatusListener((status){
if(status == AnimationStatus.completed){
animation.reverse();
}
else if(status == AnimationStatus.dismissed){
animation.forward();
}
});
animation.forward();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Center(
child: FadeTransition(
opacity: _fadeInFadeOut,
child: Container(
color: Colors.green,
width: 100,
height: 100,
),
),
),
),
);
}
}
Option #1
Simple solution and straight forward with less code, the idea is to combine FadeOut inside FadeIn and give the FadeOut delay amount greater than the duration of the FadeIn, just copy and paste then just change the Image.asset widget to anything you want to fade in/out
1- Add this package.
2- Import it in your page import 'package:animate_do/animate_do.dart';
3- Add this Widget:
Widget _animateLogo() {
return Container(
child: FadeIn(
animate: true,
duration: Duration(seconds: 2),
child: FadeOut(
animate: true,
delay: Duration(seconds: 2),
duration: Duration(seconds: 1),
// Just change the Image.asset widget to anything you want to fade in/out:
child: Image.asset(
"assets/images/logo.png",
height: 150,
width: 150,
fit: BoxFit.contain,
), //Image.asset
) // FadeOut
),
);
}
Option #2:
Use native class AnimatedOpacity class with setState:
// 1- Declare
bool _shouldFade = false;
// 2- Animation fucntion
Widget _animateLogo() {
return AnimatedOpacity(
duration: Duration(milliseconds: 200),
opacity: _shouldFade ? 1 : 0,
child: Container(
// Whatever you want to fadein fadeout
),
);
}
// 3- Animate whenever needed
setState(() {
// Fade in
_shouldFade = true;
});
// Fadeout after 3 seconds
Future.delayed(
Duration(seconds: 3),
() => setState(() {
_shouldFade = false;
}),
);