How to chain multiple Animation objects? - flutter

What i want is to chain multiple Animation object like if we have an Animation<double> that goes from 0 to 40 (let's call it the big one) and i have another 4 Animation<double> object what i want is
when the big animation starts the first animation start with it , but it end's when the big one reach 10.
when the big one reach 10 the second animation start's and end's when the big one reach 20.
etc...
any one knows how to do it in flutter ??

It sounds like staggered animations.
Basically, you can create an animation controller and set the total duration time.
Then, you can create a separate tween for each animation you want to perform. For each tween, you can define a curve, for that curve you can define an interval in the percentage of the total duration of the animation. There is a pretty good example in a flutter.dev when you search for staggered animations. Note: they don't have to be one after another, despite the name, they can be fired at the same time, but end at different time as you want. I'm not sure if it is appropriate to give an answer with just sharing the link to the flutter docs, but here it is
https://flutter.dev/docs/development/ui/animations/staggered-animations.
I have done something similar, but with 2 controllers, which I fire at the same time, but they have different durations.
Ah, I was second :)
edit: here is some code
one with 2 controlers
import 'package:flutter/material.dart';
//import 'package:flutter/scheduler.dart';
import 'package:flutter_color_picker/components/color_ripple.dart';
class ColorKnob extends StatefulWidget {
const ColorKnob({this.color, this.ratio, this.saveColor});
final Color color;
final double ratio;
final Function saveColor;
#override
_ColorKnobState createState() => _ColorKnobState();
}
class _ColorKnobState extends State<ColorKnob> with TickerProviderStateMixin {
AnimationController scaleAnimationController;
AnimationController rippleAnimationController;
Animation<double> scaleAnimation;
#override
void initState() {
super.initState();
scaleAnimationController =
AnimationController(vsync: this, duration: Duration(milliseconds: 100));
rippleAnimationController =
AnimationController(vsync: this, duration: Duration(milliseconds: 400));
scaleAnimationController.addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.completed) {
scaleAnimationController.reverse();
}
});
scaleAnimation = Tween<double>(begin: 1.0, end: 0.8).animate(
CurvedAnimation(
parent: scaleAnimationController, curve: Curves.easeOut));
rippleAnimationController.addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.completed) {
widget.saveColor();
}
});
scaleAnimation.addListener(() {
setState(() {});
});
}
#override
void dispose() {
super.dispose();
scaleAnimationController.dispose();
rippleAnimationController.dispose();
}
#override
Widget build(BuildContext context) {
return Center(
child: Container(
decoration: const BoxDecoration(
shape: BoxShape.circle, color: Colors.transparent),
child: FractionallySizedBox(
widthFactor: widget.ratio,
heightFactor: widget.ratio,
child: ClipOval(
clipBehavior: Clip.antiAlias,
child: Center(
child: Stack(children: <Widget>[
ColorRipple(
controller: rippleAnimationController,
color: widget.color,
),
GestureDetector(
onTap: () {
// timeDilation = 1.0;
scaleAnimationController.forward(from: 0.0);
rippleAnimationController.forward(from: 0.0);
},
child: Transform.scale(
scale: scaleAnimation.value,
alignment: Alignment.center,
child: Container(
width: 60.0,
height: 60.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: widget.color,
border: Border.all(
color: const Color(0xFFFFFFFF),
style: BorderStyle.solid,
width: 4.0,
),
boxShadow: const <BoxShadow>[
BoxShadow(
offset: Offset(0.0, 1.0),
blurRadius: 6.0,
spreadRadius: 1.0,
color: Color(0x44000000),
)
]),
),
),
),
]),
),
),
)),
);
}
}
and one with multiple tweens
import 'package:flutter/material.dart';
//import 'package:flutter/scheduler.dart';
class ColorRipple extends StatelessWidget {
ColorRipple({this.controller, this.color, this.size})
: scaleUpAnimation = Tween<double>(begin: 0.8, end: 5.0).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(
0.2,
1.0,
curve: Cubic(0.25, 0.46, 0.45, 0.94),
),
),
),
opacityAnimation = Tween<double>(begin: 0.6, end: 0.0).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(
0.4,
1.0,
curve: Cubic(0.25, 0.46, 0.45, 0.94),
),
),
),
scaleDownAnimation = Tween<double>(begin: 1.0, end: 0.8).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(0.0, 0.2, curve: Curves.easeOut),
),
);
final AnimationController controller;
final Animation<double> scaleUpAnimation;
final Animation<double> scaleDownAnimation;
final Animation<double> opacityAnimation;
final Color color;
final Size size;
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: controller,
builder: (BuildContext context, Widget child) {
return Container(
child: Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..scale(scaleDownAnimation.value)
..scale(scaleUpAnimation.value),
child: Opacity(
opacity: opacityAnimation.value,
child: Container(
width: 60.0,
height: 60.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: color,
style: BorderStyle.solid,
width: 4.0 - (2 * controller.value))),
)),
),
);
});
}
}

Related

lift and flip card animation in flutter

I want to lift my card up, flip it and then place it back to its original position. Currently i have achieved to flip the card by the following code. Can anyone tell how can i lift the card as well.
animation = TweenSequence(
<TweenSequenceItem<double>>[
TweenSequenceItem<double>(
tween: Tween(begin: 0.0, end: 2*pi).chain(CurveTween(curve: Curves.linear)),
weight: 50.0,
),
TweenSequenceItem<double>(
tween: ConstantTween<double>(pi*2),
weight: 50.0,
),
],
).animate(animationController);
And my widget is:-
AnimatedBuilder(
animation: animationController,
child: Container(
height: 100,
width: 100,
color: Colors.red,
),
builder: (context,child){
Matrix4 transform = Matrix4.identity();
transform.setEntry(3, 2, 0.001);
transform.rotateY(animation.value);
return Transform(
transform: transform,
alignment: Alignment.center,
child: child,
);
},
),
You can lift the container up and put it back using AnimatedContainer. Here is a sample using your snippets.
class FlipAndLiftAnimation extends StatefulWidget {
const FlipAndLiftAnimation({Key? key}) : super(key: key);
#override
_FlipAndLiftAnimationState createState() => _FlipAndLiftAnimationState();
}
class _FlipAndLiftAnimationState extends State<FlipAndLiftAnimation>
with TickerProviderStateMixin {
late AnimationController animationController;
late Animation<double> animation;
double boxSize = 100;
double initialSize = 100;
double expandedSize = 300;
#override
void initState() {
animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 350),
);
animation = TweenSequence(
<TweenSequenceItem<double>>[
TweenSequenceItem<double>(
tween: Tween(begin: 0.0, end: 2 * pi)
.chain(CurveTween(curve: Curves.linear)),
weight: 50.0,
),
TweenSequenceItem<double>(
tween: ConstantTween<double>(pi * 2),
weight: 50.0,
),
],
).animate(animationController);
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
color: const Color.fromARGB(164, 117, 81, 1),
child: Center(
child: GestureDetector(
onTap: () async {
animationController.isCompleted
? animationController.reverse()
: animationController.forward();
setState(() {
boxSize = expandedSize;
});
await Future.delayed(const Duration(milliseconds: 500), () {
setState(() {
boxSize = initialSize;
});
});
},
child: AnimatedBuilder(
animation: animationController,
child: AnimatedContainer(
margin: EdgeInsets.all(50),
height: boxSize,
width: boxSize,
color: Colors.red,
duration: Duration(milliseconds: 300),
),
builder: (context, child) {
Matrix4 transform = Matrix4.identity();
transform.setEntry(3, 2, 0.001);
transform.rotateY(animation.value);
transform.scale(1.0, 1.0, 10);
return Transform(
transform: transform,
alignment: Alignment.center,
child: child,
);
},
),
),
),
),
);
}
}

Flutter FadeTransition and backdrop filter

FadeTransition animation is not working for backdropfilter.
The opacity goes from 0 to 1 suddenly, without the smooth transition that the animation should give.
As you can see from the code below, I have a background image and then a backdropfilter on top to blur the background at app startup.
(I then show other widgets, but the animation is working fine for them).
import 'dart:ui';
import 'package:flutter/material.dart';
class SplashScreen extends StatefulWidget {
#override
_SplashScreenState createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen>
with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<double> _backgroundOpacity;
bool _visible = true;
#override
void initState() {
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
)..forward();
// background opacity animation
_backgroundOpacity = Tween<double>(
begin: 0,
end: 1,
).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0, 1, curve: Curves.linear),
),
);
super.initState();
}
#override
void dispose() {
super.dispose();
_controller.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.green[800],
),
body: Stack(
children: [
Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Image(
image: AssetImage('assets/field.jpg'),
fit: BoxFit.cover,
),
),
FadeTransition(
opacity: _backgroundOpacity,
child: Container(
child: BackdropFilter(
filter: new ImageFilter.blur(sigmaX: 6.0, sigmaY: 6.0),
child: new Container(
decoration: new BoxDecoration(
color: Colors.grey.shade200.withOpacity(0.5),
),
),
),
),
),
],
),
);
}
}
basically what I get from this code is the background image clear and nice, then after 2 seconds suddenly blurred, without any transition.
What should I do to make fadeTransition to work with Backdropfilter?
The preferred way to fade in a blur is to use a TweenAnimationBuilder:
TweenAnimationBuilder<double>(
duration: Duration(milliseconds: 500),
tween: Tween<double>(begin: 0, end: 6),
builder: (context, value, _) {
return BackdropFilter(
key: GlobalObjectKey('background'),
filter: ImageFilter.blur(sigmaY: value, sigmaX: value),
child: Container(
decoration: new BoxDecoration(
color: Colors.grey.shade200.withOpacity(0.5),
),
),
);
},
),

Animating the front card to the back in a stack view of cards in flutter

I have got a stack of cards and scroll wheel. I am trying to animate the front card, move it to the right, then bring it back in a way it goes to the back of the cards.
I have used a future function to specify witch part of code should be done first. But what I get is; it changes the index of the card first the animation takes place. Here is my code:
class AnimationsPractice extends StatefulWidget {
static const String id = 'test_screen';
#override
_AnimationsPracticeState createState() => _AnimationsPracticeState();
}
class _AnimationsPracticeState extends State<AnimationsPractice>
with SingleTickerProviderStateMixin {
FixedExtentScrollController _scrollController =
FixedExtentScrollController(initialItem: 0);
AnimationController _controller;
Animation<Offset> _offsetAnimation;
int selected;
List<Widget> sampleCard;
Animation<Offset> _offsetAnimation2;
bool halfWayAnimation;
#override
void initState() {
super.initState();
_controller =
AnimationController(duration: const Duration(seconds: 1), vsync: this)
..repeat();
_offsetAnimation = Tween<Offset>(
begin: Offset.zero,
end: const Offset(1.5, 0.0),
).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(
0.0,
0.5,
curve: Curves.elasticIn,
),
),
);
_offsetAnimation2 = Tween<Offset>(
begin: const Offset(1.5, 0.0),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(
0.5,
1.0,
curve: Curves.elasticIn,
),
),
);
halfWayAnimation = false;
_controller.stop();
sampleCard = [
Container(
height: 60,
width: 40,
color: Colors.red,
),
Transform.rotate(
angle: 10 * (pi / 180),
child: Container(
height: 60,
width: 40,
color: Colors.blueGrey,
)),
SlideTransition(
position: halfWayAnimation ? _offsetAnimation2 : _offsetAnimation,
child: Container(
height: 60,
width: 40,
color: Colors.yellow,
),
),
];
}
#override
void dispose() {
super.dispose();
_controller.dispose();
}
Future<void> _playAnimation() async {
try {
await _controller.forward().orCancel;
await siftingIndex();
await _controller.reverse().orCancel;
} on TickerCanceled {
// the animation got canceled, probably because it was disposed of
}
}
Future<void> siftingIndex() {
return Future.delayed(const Duration(microseconds: 200), () {
sampleCard.insert(0, sampleCard[sampleCard.length - 1]);
sampleCard.removeLast();
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(180.0),
child: SafeArea(
child: TextButton(
child: Text('back to login'),
onPressed: () {
Navigator.pushNamed(context, LoginScreen.id);
},
),
),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Stack(children: sampleCard),
CustomScrollWheel(
onItemChange: (x) {
setState(() {
_playAnimation();
halfWayAnimation = true;
});
},
scrollController: _scrollController,
),
],
),
);
}
}
enter image description here

Flutter. How to make AnimationBuilder listen to Multiple Animations?

class _SomeWidgetWithAnimationsState extends State<SomeWidgetWithAnimations> with TickerProviderStateMixin {
AnimationController firstController;
AnimationController secondController;
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
aniamtion: /* Here I want to pass the two animation controllers above */,
builder: (context, child) => /* Something that uses two animations to animate */,
);
}
}
I want to be able to listen to events from multiple Listenables where only one Listenable is required. Is there a way to route notifications from two Listenables to one? I considered creating my own implementation of the Listenable that will have some method such as addSourceListenable(Listenable source) that will subscribe to the Listenable source with a callback that will notify its own subscribers. But I think there is a more elegant way to solve this. Maybe RxDart can offer something.
For this use case having multiple animations flutter has an own concept called Staggered Animations. You can read more about it here:
https://flutter.dev/docs/development/ui/animations/staggered-animations#:~:text=To%20create%20a%20staggered%20animation,being%20animated%2C%20create%20a%20Tween%20.
This is a full working example provided from the article:
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart' show timeDilation;
class StaggerAnimation extends StatelessWidget {
StaggerAnimation({ Key key, this.controller }) :
// Each animation defined here transforms its value during the subset
// of the controller's duration defined by the animation's interval.
// For example the opacity animation transforms its value during
// the first 10% of the controller's duration.
opacity = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(
CurvedAnimation(
parent: controller,
curve: Interval(
0.0, 0.100,
curve: Curves.ease,
),
),
),
width = Tween<double>(
begin: 50.0,
end: 150.0,
).animate(
CurvedAnimation(
parent: controller,
curve: Interval(
0.125, 0.250,
curve: Curves.ease,
),
),
),
height = Tween<double>(
begin: 50.0,
end: 150.0
).animate(
CurvedAnimation(
parent: controller,
curve: Interval(
0.250, 0.375,
curve: Curves.ease,
),
),
),
padding = EdgeInsetsTween(
begin: const EdgeInsets.only(bottom: 16.0),
end: const EdgeInsets.only(bottom: 75.0),
).animate(
CurvedAnimation(
parent: controller,
curve: Interval(
0.250, 0.375,
curve: Curves.ease,
),
),
),
borderRadius = BorderRadiusTween(
begin: BorderRadius.circular(4.0),
end: BorderRadius.circular(75.0),
).animate(
CurvedAnimation(
parent: controller,
curve: Interval(
0.375, 0.500,
curve: Curves.ease,
),
),
),
color = ColorTween(
begin: Colors.indigo[100],
end: Colors.orange[400],
).animate(
CurvedAnimation(
parent: controller,
curve: Interval(
0.500, 0.750,
curve: Curves.ease,
),
),
),
super(key: key);
final Animation<double> controller;
final Animation<double> opacity;
final Animation<double> width;
final Animation<double> height;
final Animation<EdgeInsets> padding;
final Animation<BorderRadius> borderRadius;
final Animation<Color> color;
// This function is called each time the controller "ticks" a new frame.
// When it runs, all of the animation's values will have been
// updated to reflect the controller's current value.
Widget _buildAnimation(BuildContext context, Widget child) {
return Container(
padding: padding.value,
alignment: Alignment.bottomCenter,
child: Opacity(
opacity: opacity.value,
child: Container(
width: width.value,
height: height.value,
decoration: BoxDecoration(
color: color.value,
border: Border.all(
color: Colors.indigo[300],
width: 3.0,
),
borderRadius: borderRadius.value,
),
),
),
);
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
builder: _buildAnimation,
animation: controller,
);
}
}
class StaggerDemo extends StatefulWidget {
#override
_StaggerDemoState createState() => _StaggerDemoState();
}
class _StaggerDemoState extends State<StaggerDemo> with TickerProviderStateMixin {
AnimationController _controller;
#override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 2000),
vsync: this
);
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
Future<void> _playAnimation() async {
try {
await _controller.forward().orCancel;
await _controller.reverse().orCancel;
} on TickerCanceled {
// the animation got canceled, probably because we were disposed
}
}
#override
Widget build(BuildContext context) {
timeDilation = 10.0; // 1.0 is normal animation speed.
return Scaffold(
appBar: AppBar(
title: const Text('Staggered Animation'),
),
body: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
_playAnimation();
},
child: Center(
child: Container(
width: 300.0,
height: 300.0,
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.1),
border: Border.all(
color: Colors.black.withOpacity(0.5),
),
),
child: StaggerAnimation(
controller: _controller.view
),
),
),
),
);
}
}
void main() {
runApp(MaterialApp(home: StaggerDemo()));
}
You can nearly do everything and its chained in the logic. I recomend you just guiding threw the docs there, its well explained.
Have a look at Listenable.merge(List<Listenable?> listenables) which returns a Listenable that triggers when any of the given Listenables themselves trigger.
Hope i have helped you :)
https://api.flutter.dev/flutter/foundation/Listenable/Listenable.merge.html

How to pass animation value to a child widget

I have a widget that defines an animation. This animation progresses from 0.0 to 1.0. This widget also has a child widget, whose opacity I'd like to control in sync with the animation progress. However, I don't see how I can have the child widget's state "track" the state of the parent's animation progress. Whatever I give from the parent to the child ends up being final and, thus, immutable.
I tried passing the Animation directly, but then the app crashes with error "The getter value was called on null".
Edit: with code. My problem is that the MenuItem class can't be made aware of the states of the Animation values.
import 'package:flutter/material.dart';
class MenuItem extends StatefulWidget {
final Function() onPressed;
final String tooltip;
final String helper;
final IconData icon;
MenuItem({this.onPressed, this.tooltip, this.helper, this.icon});
#override
_MenuItemState createState() => _MenuItemState();
}
class _MenuItemState extends State<MenuItem> {
bool isOpened = true;
double _elevateButtonValue = 0.0;
#override
Widget build(BuildContext context) {
return Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
AnimatedOpacity(
opacity: isOpened ? 1.0 : 0.0,
duration: Duration(milliseconds: 500),
child: new Container(
child: Text(widget.helper),
decoration: new BoxDecoration (
borderRadius: new BorderRadius.all(const Radius.circular(8.0)),
color: Colors.white,
boxShadow: [new BoxShadow(
color: Colors.black.withOpacity(0.3),
offset: Offset(0.0, 6.0),
blurRadius: 16.0,
),
],
),
padding: new EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 8.0),
),
),
SizedBox(width: 12.0),
FloatingActionButton(
onPressed: () {},
tooltip: widget.tooltip,
child: Icon(widget.icon),
backgroundColor: Colors.deepOrange,
elevation: _elevateButtonValue,
),
],
),
);
}
}
class MenuFabs extends StatefulWidget {
#override
_MenuFabsState createState() => _MenuFabsState();
}
class _MenuFabsState extends State<MenuFabs>
with SingleTickerProviderStateMixin {
bool isOpened = false;
AnimationController _animationController;
Animation<Color> _buttonColor;
Animation<double> _animateIcon;
Animation<double> _translateButton;
Animation<double> _elevateButton;
Curve _curve = Curves.easeOut;
double _fabHeight = 56.0;
#override
initState() {
_animationController =
AnimationController(vsync: this, duration: Duration(milliseconds: 500))
..addListener(() {
setState(() {});
});
_animateIcon =
Tween<double>(begin: 0.0, end: 1.0).animate(_animationController);
_buttonColor = ColorTween(
begin: Colors.deepOrange,
end: Colors.black45,
).animate(CurvedAnimation(
parent: _animationController,
curve: Interval(
0.00,
1.00,
curve: Curves.linear,
),
));
_translateButton = Tween<double>(
begin: _fabHeight,
end: -14.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Interval(
0.0,
0.75,
curve: _curve,
),
));
_elevateButton = Tween<double>(
begin: 0.0,
end: 6.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Interval(
0.25,
1.0,
curve: _curve,
),
));
super.initState();
}
#override
dispose() {
_animationController.dispose();
super.dispose();
}
animate() {
if (!isOpened) {
_animationController.forward();
} else {
_animationController.reverse();
}
isOpened = !isOpened;
}
Widget goal() {
return Container(
child: new MenuItem(
onPressed: () {},
tooltip: 'Geolocate',
helper: 'Geolocate',
icon: Icons.radio_button_checked,
),
);
}
Widget invite() {
return Container(
child: new MenuItem(
onPressed: () {},
tooltip: 'Invite friends',
helper: 'Invite friends',
icon: Icons.person_add,
),
);
}
Widget toggle() {
return Container(
child: FloatingActionButton(
backgroundColor: _buttonColor.value,
onPressed: animate,
tooltip: 'Toggle',
child: AnimatedIcon(
icon: AnimatedIcons.menu_close,
progress: _animateIcon,
),
),
);
}
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Transform(
transform: Matrix4.translationValues(
0.0,
_translateButton.value * 2.0,
0.0,
),
child: goal(),
),
Transform(
transform: Matrix4.translationValues(
0.0,
_translateButton.value,
0.0,
),
child: invite(),
),
Transform(
transform: Matrix4.translationValues(
168.0,
0.0,
0.0,
),
child: toggle(),
),
],
);
}
}
Adding with SingleTickerProviderStateMixin to the child Widget made the error go away!