The Proper Way of Doing Chain Animations

The proper way of doing chain animations

__block NSMutableArray* animationBlocks = [NSMutableArray new];
typedef void(^animationBlock)(BOOL);

// getNextAnimation
// removes the first block in the queue and returns it
animationBlock (^getNextAnimation)() = ^{

if ([animationBlocks count] > 0){
animationBlock block = (animationBlock)[animationBlocks objectAtIndex:0];
[animationBlocks removeObjectAtIndex:0];
return block;
} else {
return ^(BOOL finished){
animationBlocks = nil;
};
}
};

[animationBlocks addObject:^(BOOL finished){
[UIView animateWithDuration:duration delay:0.0 options:UIViewAnimationOptionCurveLinear animations:^{
//my first set of animations
} completion: getNextAnimation()];
}];

[animationBlocks addObject:^(BOOL finished){
[UIView animateWithDuration:duration delay:0.0 options:UIViewAnimationOptionCurveLinear animations:^{
//second set of animations
} completion: getNextAnimation()];
}];

[animationBlocks addObject:^(BOOL finished){
[UIView animateWithDuration:duration delay:0.0 options:UIViewAnimationOptionCurveLinear animations:^{
//third set
} completion: getNextAnimation()];
}];

[animationBlocks addObject:^(BOOL finished){
[UIView animateWithDuration:duration delay:0.0 options:UIViewAnimationOptionCurveLinear animations:^{
//last set of animations
} completion:getNextAnimation()];
}];

// execute the first block in the queue
getNextAnimation()(YES);

Chaining Multiple CSS Animations

The problem is actually not with the order of the animations but because of how multiple animations on pme element works. When multiple animations are added on an element, they start at the same time by default.

Because of this, both the laydown and falling animations start at the same time but the laydown animation actually completes within 1000ms from the start but the first animation (which is falling) doesn't complete till 2000ms.

The W3C spec about animations also say the following about multiple animations accessing the same property during animation:

If multiple animations are attempting to modify the same property, then the animation closest to the end of the list of names wins.

In the code provided in question, both animations are trying to modify the transform property and the second animation is the closest to the end. So while the second animation is still running (which is, for the first 1000ms) the transform changes are applied as specified in the second animation. During this time the first animation is still running but it has no effect because its values are overwritten. In the 2nd 1000ms (when the second animation has already completed but 1st is still executing), the transforms are applied as directed by the first animation. This is why it looks as if the second animation is running before the first animation and then the first.


To fix this problem, the execution of the second animation should be put on hold (or delayed) until the time the first animation is complete. This can be done by adding a animation-delay (that is equal to the animation-duration of the first animation) for the second animation.

animation-name: falling, laydown;
animation-duration: 2000ms, 1000ms;
animation-delay: 0ms, 2000ms; /* add this */
animation-timing-function: ease-in, ease-out;
animation-iteration-count: 1, 1;

html,body {  height: 100%;}body {  display: flex;  align-items: center;  justify-content: center;}@keyframes falling {  0% {    transform: translate3d(0, -400px, 0);  }  100% {    transform: translate3d(0, 40%, 0) rotateX(30deg) rotateY(0deg) rotateZ(60deg);  }}@keyframes laydown {  0% {    transform: translate3d(0, 40%, 0) rotateX(30deg) rotateY(0deg) rotateZ(60deg);  }  100% {    transform: translate3d(0, 40%, 0) rotateX(70deg) rotateY(0deg) rotateZ(80deg);  }}#falling-card-parent {  height: 150px;  width: 100px;  margin: auto;  perspective: 1000px;}#falling-card {  height: 150px;  width: 100px;  background-color: black;  margin: auto;  transform: translate3d(0, 40%, 0) rotateX(70deg) rotateY(0deg) rotateZ(80deg);  animation-name: falling, laydown;  animation-duration: 2000ms, 1000ms;  animation-delay: 0ms, 2000ms;  animation-timing-function: ease-in, ease-out;  animation-iteration-count: 1, 1;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/prefixfree/1.0.7/prefixfree.min.js"></script><div id="falling-card-parent">  <div id="falling-card"></div></div>

How to chain animation and non-animation functions in Flutter?

I have implemented this using both callback functions and a stack since I felt that this would give me the most flexibility, e.g. if I wanted to hide/show the Text widgets with different start/end times to give it a more organic feel. This works, but as per my original question I am open to suggestions if there is a better way to implement this.

The basic execution workflow in pseudo-code is:

Grid Display

    shuffle.onPressed() {
disable user input;
iterate over the grid {
if (cell contains a text value) {
push Text widget key onto a stack (List);
trigger the hide animation (pass callback #1);
}
}
}

Text widget hide animation

    hide animation.whenComplete() {
call the next function (callback #1 - pass widget key);
}

Callback function #1

    remove Text widget key from the stack;
if (stack is empty) {
executive shuffle function;
iterate over the grid;
if (cell contains a text value) {
push Text widget key onto a stack (List);
trigger the show animation (pass callback #2);
}
}

Text widget show animation

    show animation.whenComplete() {
call the next function (callback #2 - pass widget key);
}

Callback function #2

    remove Text widget key from the stack
if (stack is empty) {
enable user input;
}

I have included extracts of the code below to show how I have implemented this.

The main class showing the grid on screen has the following variables and functions.

    class GridState extends State<Grid> {
// List containing Text widgets to displays in cells including unique
// keys and text values
final List<TextWidget> _letterList = _generateList(_generateKeys());

// Keys of animated widgets - used to track when these finish
final List<GlobalKey<TextWidgetState>> _animations = [];

bool _isInputEnabled = true; // Flag to control user input

@override
Widget build(BuildContext context) {

ElevatedButton(
onPressed: () {
if (_isInputEnabled) {
_hideTiles();
}
},
child: Text('shuffle', style: TextStyle(fontSize: _fontSize)),
),

}

// Function to hide the tiles on the screen using their animation
void _hideTiles() {
_isInputEnabled = false; // Disable user input

// Hide the existing tiles using animation
for (int i = 0; i < _letterList.length; i++) {
// Only animate if the tile has a text value
if (_letterList[i].hasText()) {
_animations.add(_letterList[i].key as GlobalKey<LetterTileState>);
_letterList[i].hide(_shuffleAndShow);
}
}
}

// Function to shuffle the text on screen and then re-show the tiles using
// their animations
void _shuffleAndShow() {
_animations.remove(key);

if (_animations.isEmpty) {
widget._letterGrid.shuffleText(
widget._letterGrid.getInputText(), widget._options.getCharType());

// Update the tiles with the new characters and show the new tile locations using animation
for (int i = 0; i < _letterList.length; i++) {
// Update tile with new character
_letterList[i].setText(
widget._letterGrid.getCell(i, widget._options.getCharType()));

// If the tile has a character then animate it
if (_letterList[i].hasText()) {
_animations.add(_letterList[i].key as GlobalKey<LetterTileState>);
_letterList[i].show(_enableInput);
}
}
}
}

// Function re-enable user input following the shuffle animations
void _enableInput(GlobalKey<LetterTileState> key) {
_animations.remove(key);

if (_animations.isEmpty) {
_isInputEnabled = true;
}
}

The Text widgets held in _letterList have the following animation functions, which call the callback function when they have finished. Note this code is in the State of a Statefulwidget.


// Animation variables
final Duration _timer = const Duration(milliseconds: 700);
late AnimationController _animationController;
late Animation<double> _rotateAnimation;
late Animation<double> _scaleAnimation;

@override
void initState() {
super.initState();

// Set up the animation variables
_animationController = AnimationController(vsync: this, duration: _timer);

_rotateAnimation = Tween<double>(begin: 0, end: 6 * pi).animate(
CurvedAnimation(
parent: _animationController,
curve: const Interval(0, 1, curve: Curves.easeIn)));
_rotateAnimation.addListener(() {
setState(() {});
});

_scaleAnimation = Tween<double>(begin: 1, end: 0).animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0, 0.95, curve: Curves.ease)));
_scaleAnimation.addListener(() {
setState(() {});
});
}

///
/// Animation functions
///
// Function to hide the tile - spin and shrink to nothing
void hide(Function callback) {s
_animationController.forward(from: 0).whenComplete(() {
_animationController.reset();
callback(widget.key);
});
}

// Function to show the tile - spin and grow from nothing
void show(Function callback) {
_animationController.reverse(from: 1).whenComplete(() {
_animationController.reset();
callback(widget.key);
});
}

UPDATE:

Having done more reading and built an experimental app using the default counter example, I have found that added Listeners and StatusListeners are another, perhaps better, way to do what I want. This would also work with the stack approach I used in my earlier answer as well.

Example code below:

main class:

    import 'package:flutter/material.dart';

import 'counter.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);

// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);

final String title;

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
Counter counter = Counter();
late AnimationController animationController;
late Animation<double> shrinkAnimation;

@override
void initState() {
animationController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 500));

shrinkAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(
CurvedAnimation(
parent: animationController,
curve: const Interval(0.0, 1.0, curve: Curves.linear)));
shrinkAnimation.addListener(() {
setState(() {}); // Refresh the screen
});
shrinkAnimation.addStatusListener((status) {
switch (status) {
// Completed status is after the end of forward animation
case AnimationStatus.completed:
{
// Increment the counter
counter.increment();

// Do some work that isn't related to animation
int value = 0;
for (int i = 0; i < 1000; i++) {
value++;
}
print('finishing value is $value');

// Then reverse the animation
animationController.reverse();
}
break;
// Dismissed status is after the end of reverse animation
case AnimationStatus.dismissed:
{
animationController.reset();
}
break;
}
});
super.initState();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
AnimatedBuilder(
animation: animationController,
builder: (context, child) {
return Transform.scale(
alignment: Alignment.center,
scale: shrinkAnimation.value,
child: Text(
'${counter.get()}',
style: Theme.of(context).textTheme.headline4,
),
);
}),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
animationController.forward(); // Shrink current value first
},
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}

counter class:

    import 'package:flutter/cupertino.dart';

class Counter {
int _counter = 0;

void increment() {
_counter++;
}

int get() {
return _counter;
}
}

Best way to handle a chain of animations with UIViewAnimations

You can try to use CAAnimation I think it will be easier to you to manage colors and animations

- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSArray *colors = @[[UIColor greenColor],
[UIColor redColor],
[UIColor blueColor],
[UIColor yellowColor],
[UIColor orangeColor]
];
NSMutableArray *animations = [NSMutableArray new];
float colorAnimationDuration = 3.0f;
for (int i = 0; i < colors.count; i++)
{
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"backgroundColor"];
animation.toValue = (id)[[colors objectAtIndex:i] CGColor];
animation.duration = colorAnimationDuration;
animation.beginTime = i * colorAnimationDuration;
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;

[animations addObject:animation];
}

CAAnimationGroup *animationsGroup = [CAAnimationGroup new];
animationsGroup.duration = colors.count * colorAnimationDuration;
animationsGroup.repeatCount = HUGE_VALF;
animationsGroup.animations = animations;

[self.view.layer addAnimation:animationsGroup forKey:@"myAnimations"];

}

- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self.view.layer removeAnimationForKey:@"myAnimations"];
}

How to i chain two parts of an animation together in jetpack compose so the the offset increases then decreases?

The correct answer is to use Kotlin coroutines. I managed to get it working fine. You have to use coroutines in order to launch the animations in the correct sequence like this:

animationRoutine.launch {
coroutineScope {
launch {
animate(
startingValue,
targetValue,
animationSpec = whatYouWant,
block = { value, _ -> whateverYouNeed = value }
)
}
launch {
animate(
initialValue,
targetValue,
animationSpec = whatYouWant,
block = { value, _ -> whateverYouNeed = value }
)
}
}

Each of launch scope launches everything in a non blocking way if you tell it to allowing you to run multiple animations at once at a lower level and to sequence the animations you add another coroutine for the next part of the animation.

How to chain different CAAnimation in an iOS application

tl;dr: You need to manually add each animation after the previous finishes.


There is no built in way to add sequential animations. You could set the delay of each animation to be the sum of all previous animations but I wouldn't recommend it.

Instead I would create all the animations and add them to a mutable array (using the array as a queue) in the order they are supposed to run. Then by setting yourself as the animations delegate to all the animations you can get the animationDidStop:finished: callback whenever an animation finishes.

In that method you will remove the first animation (meaning the next animation) from the array and add it to the layer. Since you are the delegate you will get a second animation when that one finishes in which case the animationDidStop:finished: callback will run again and the next animation is removed from the mutable array and added to the layer.

Once the array of animations is empty, all animations will have run.


Some sample code to get you started. First you set up all your animations:

CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"backgroundColor"];
[animation setToValue:(id)[[UIColor redColor] CGColor]];
[animation setDuration:1.5];
[animation setDelegate:self];
[animation setValue:[view layer] forKey:@"layerToApplyAnimationTo"];

// Configure other animations the same way ...

[self setSequenceOfAnimations:[NSMutableArray arrayWithArray: @[ animation, animation1, animation2, animation3, animation4, animation5 ] ]];

// Start the chain of animations by adding the "next" (the first) animation
[self applyNextAnimation];

Then in the delegate callback you simply apply the next animation again

- (void)animationDidStop:(CAAnimation *)animation finished:(BOOL)finished {
[self applyNextAnimation];
}

- (void)applyNextAnimation {
// Finish when there are no more animations to run
if ([[self sequenceOfAnimations] count] == 0) return;

// Get the next animation and remove it from the "queue"
CAPropertyAnimation * nextAnimation = [[self sequenceOfAnimations] objectAtIndex:0];
[[self sequenceOfAnimations] removeObjectAtIndex:0];

// Get the layer and apply the animation
CALayer *layerToAnimate = [nextAnimation valueForKey:@"layerToApplyAnimationTo"];
[layerToAnimate addAnimation:nextAnimation forKey:nil];
}

I'm using a custom key layerToApplyAnimationTo so that each animation knows its layer (it works just by setValue:forKey: and valueForKey:).

CSS chain multiple animations from different classes

There must be a better way probably but that is what i got and i think it will solve your problem

So i collect boxes x and y coordinates when animation button clicks and after the animationend event, i collect both x and y coordinates again and calculate difference of them and add to box style.

const firstButton = document.querySelectorAll('button')[0];

const secondButton = document.querySelectorAll('button')[1];

const myBox = document.querySelector('div')

let rectBefore;
let rectAfter;
let positionBeforeX;
let positionBeforeY;
let positionAfterX;
let positionAfterY;

let differenceX;
let differenceY;

firstButton.addEventListener('click', () => {
rectBefore = myBox.getBoundingClientRect();
positionBeforeX = rectBefore.left;
positionBeforeY = rectBefore.top;

myBox.classList.toggle('first');
})

secondButton.addEventListener('click', () => {
rectBefore = myBox.getBoundingClientRect();
positionBeforeX = rectBefore.left;
positionBeforeY = rectBefore.top;

myBox.classList.toggle('second');

})

myBox.addEventListener('animationend', (event) =>{
rectAfter = myBox.getBoundingClientRect();
positionAfterX = rectAfter.left;
positionAfterY = rectAfter.top;

differenceX = positionAfterX - positionBeforeX;
differenceY = positionAfterY - positionBeforeY;

if(myBox.style.left !== ""){
myBox.style.left = `${parseInt(myBox.style.left.split('px')) + differenceX}px`;
myBox.style.top = `${parseInt(myBox.style.top.split('px')) + differenceY}px`;
}
else{
myBox.style.left = `${differenceX}px`;
myBox.style.top = `${differenceY}px`;
}

myBox.classList.remove(`${event.animationName}`);
})
*,
*::before,
*::after {
box-sizing: border-box;
}

body{
min-height: 100vh;
position: relative;
display: grid;
place-content: center;
}

button{
position: absolute;
background-color: greenyellow;
width: 5rem;
height: 5rem;

}

button:nth-of-type(1){
top:5rem;
right: 10rem;
margin-right: 1rem;
}

button:nth-of-type(2){
top:5rem;
right: 5rem;
}

.box{
position:relative;
width: 100px;
height: 100px;
background-color: blue;

}

.first {
animation: first 3.0s linear forwards;
}

.second {
animation: second 3.0s linear forwards;
}

@keyframes first {
20% {
transform: translate(50px, 0px);
}
40% {
transform: translate(50px, 0px) rotate(-90deg);
}
80% {
transform: translate(50px, -130px) rotate(-90deg);
}
100% {
transform: translate(50px, -125px) rotate(0deg);
}
}

@keyframes second {
20% {
transform: translate(100px, 0px);
}
40% {
transform: translate(100px, 0px) rotate(-90deg);
}
100% {
transform: translate(100px, -50px) rotate(-90deg);
}
}
<div class="box"></div>

<button>First Animation</button>
<button>Second Animation</button>

Chaining seperate animations that work on the same properties

You can use addStatusListener on your Animation. Check when the animation is completed and then call reverse() on your AnimationController.

If you want to, you can call reverse() inside a Future.delayed() for making a pause.

I've made this example for you:

import 'package:flutter/material.dart';

class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
AnimationController _animationController;
Animation _opacityDontWorry;

@override
void initState() {
super.initState();

_animationController = AnimationController(duration: Duration(seconds: 1), vsync: this);
_opacityDontWorry = Tween(
begin: 0.0,
end: 1.0,
).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeIn),
)..addStatusListener((status) {
if (status == AnimationStatus.completed) {
Future.delayed(Duration(seconds: 3), () {
_animationController.reverse();
});
}
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton.extended(
label: Text('Animate'),
onPressed: () => _animationController.forward(),
),
body: Center(
child: AnimatedBuilder(
animation: _opacityDontWorry,
builder: (context, widget) {
return Opacity(
opacity: _opacityDontWorry.value,
child: Text("Don't worry"),
);
},
),
),
);
}
}

UPDATE

In case you need to play this animation, and call another one after that, you can extract the opacity value to a variable. Then update that value from as many consecutive animations as you need.

  _firstAnimation = Tween(
begin: 0.0,
end: 1.0,
).animate(
CurvedAnimation(parent: _animationController, curve: Interval(0.0, 0.20, curve: Curves.easeIn)),
)..addListener(() {
setState(() => _opacity = _firstAnimation.value);
});

// Leave an interval pause if you need
_secondAnimation = Tween(
begin: 1.0,
end: 0.0,
).animate(
CurvedAnimation(parent: _animationController, curve: Interval(0.40, 0.60, curve: Curves.easeIn)),
)..addListener(() {
setState(() => _opacity = _secondAnimation.value);
});

In your widget's opacity property, instead of using _firstAnimation.value use _opacity.

Run a chain animation on different selectors by clicking once on a button

Easiest way would be to use JavaScript to add a class to the second target whenever the first one is clicked.

var btn1 = document.getElementById('btnq1');
var btn2 = document.getElementById('btnq2a');
btn1.addEventListener('click', makeActive);

function makeActive(){
var className = ' ' + btn2.className + ' ';
// from SO Question #14615712
if ( ~className.indexOf(' active ') ) {
btn2.className = className.replace(' active ', ' ');
} else {
btn2.className += ' active ';
}
}

Or jQuery if you prefer:

$('#btnq1').on('click', function () {
$('#btnq2a').toggleClass('active');
});

Then, adjust your CSS selector a bit.

#btnq2a.active {
animation-name:q2show;
animation-iteration-count:1;
animation-delay:4s;
animation-duration:4s;
animation-fill-mode:forwards;
}

Here's an updated FIDDLE



Related Topics



Leave a reply



Submit