Repeating a repeating animation with delay with reanimated - react-native

Using react-native-reanimated, I'm trying to repeat infinitely an animation which is also a repeated animation, with a delay.
With my code, the delay and the nested repeat animation are triggered but not the inifinite one.
Any ideas ?
useEffect(() => {
progress.value = withRepeat(
withDelay(2000, withRepeat(withTiming(0, { duration: 500 }), 6, true)),
-1,
true
);
}, []);

Like #Abe pointed out in his answer, reverse property of withRepeat is not supported when wrapped with other animation modifiers.
But, you can do this without setInterval - making use of withSequence to simulate the `reverse animation
// starting delay of 2000ms
// play animation 6 times
// repeat
progress.value =
withRepeat(
withDelay(2000, withRepeat(
withSequence(
// split duration of 500ms to 250ms
withTiming(goToValue, {duration: 250, easing: Easing.inOut(Easing.ease)}),
withTiming(initialValue, {duration: 250, easing: Easing.inOut(Easing.ease)}),
)
, 6))
, -1)

You've set the outer withRepeat to 1, so it should only repeat once, is that intended? Use a negative number to repeat indefinitely.
The docs for withRepeat say that withRepeat with the third argument (reverse) set to true doesn't work properly for the withDelay and withSequence animations, that could also be causing an issue. You might want to try reversing the animations manually in a withSequence call and repeating that.

No solution found with Reanimated, but as suggested by #Abe,a simple setInterval does the trick
useEffect(() => {
progress.value = withRepeat(withTiming(0, { duration: 400 }), 6, true);
const interval = setInterval(() => {
progress.value = withRepeat(withTiming(0, { duration: 400 }), 6, true);
}, 6000);
return () => clearInterval(interval);
}, []);

You can achieve that without setInterval, put withDelay on each animation.
withRepeat(
withSequence(
withDelay(
2000,
withTiming(0, {
duration: 300,
easing: Easing.inOut(Easing.ease),
}),
),
withDelay(
2000,
withTiming(1, {
duration: 300,
easing: Easing.inOut(Easing.ease),
}),
),
),
-1,
);

You can achieve this by using Animated.sequence
This code basically works by re-run the function when the animation done
function fadeAnimation() {
Animated.sequence([
Animated.timing(progress.value, {
toValue: 0,
duration: 500,
delay: 1000,
useNativeDriver: true,
}),
Animated.timing(progress.value, {
toValue: 1,
duration: 500,
useNativeDriver: true,
}),
]).start(() => {
fadeAnimation();
});
}
useEffect(() => {
fadeAnimation()
}, []);

Related

React Native (Animations and setState)

I am facing a particular issue related to react native animations with setState (functional component), I have a countdown made with setInterval and each second I make a setState, whenever I have a setState the animation resets and restarts and I don't know why, I am using also useRef like this
const opacity = new Animated.Value(0) const animatedValueRef = useRef(opacity), the animations is looping like this (each 250millis)
Animated.timing(animatedValueRef.current, {
toValue: 1,
duration: 220,
easing: Easing.ease,
useNativeDriver: true,
}).start((event) => {
if(event.finished) {
opacity.setValue(0);
second();
}
});
}
Thank you!
Edit: this is how I implemented the countdown:
function step1(max, onoff) {
let intervalId;
let varCounter = 1;
setAnimation(true); //animation starts
irrigation(); //animation call
let counter = function () {
if (varCounter < max) {
varCounter= varCounter + 1;
setCounter(varCounter + " " + onoff)
} else {
clearInterval(intervalId);
setCounter(" ");
setAnimation(false);
}
};
intervalId = setInterval(()=>{counter()}, 1000);
}
(The code needs to be refactored)
Basically your component is re- render every time your component’s state changes.
The component gets the updated state and React decides if it should re-render the component.
By default React re-renders everything all the time.
all you have to do is chain your animation. Your animated value will go from 0 to 1, then the second animation will make the value from 1 to 0. You just need to start the animation again after 1 second in your counter function. I suggest making the useNativeDriver: false. No need to setState at all.
Animated.timing(animatedValueRef.current, {
toValue: 1,
duration: 220,
easing: Easing.ease,
useNativeDriver: true,
}).start(() => {
Animated.timing(animatedValueRef.current, {
toValue: 0,
duration: 220,
easing: Easing.ease,
useNativeDriver: true,
}).start(())
});
}

How to provide callbacks to each animation within a Parallel call

Start method takes a callback to be run when the animation is done. But, if I have multiple animations within a Parallel call and each executes over a different duration, with one start() call on the outer Animated.Parallel() call, how do I provide individual callbacks for each animation within?
Animated.parallel([
Animated.timing(
this.pos,
{
toValue: { x: 146, y: 10 },
duration: 1500,
easing: Easing.ease
}
), // I want to run a different callback here
Animated.timing(
this.pos2,
{
toValue: { x: 146, y: 10 },
duration: 2500,
easing: Easing.quad
}
), // ...and here
]).start(function () {
// code for execution after both animations are finished.
});
I accomplished what I wanted by using a sequence within the parallel call.

Animate rotation to the nearest value in React Native

Given, interpolation, that has to rotate a car marker:
this.rotation.interpolate({
inputRange: [0, 360],
outputRange: ['0deg', '360deg'],
extrapolate: 'clamp'
})
GIF below represents the following bearings:
1. 337.38055647641903
2. 335.202973299875
3. 13.03761189748721
4. 13.042704554861551
5. 358.77805501498045
Green line is how I want it to work (rotate to the nearest value). Red line is how it works currently (on GIF).
Is there a way to animate rotation to the nearest value? (in our case from 335 to 13 clockwise). Maybe there's a better way instead of interpolation?
let heading = this.state.heading;
let oldHeading = this.state.oldHeading;
const duration = 200;
if (oldHeading - heading > 180) {
Animated.timing(
this.state.spinAnim,
{
toValue: 360,
duration: duration,
easing: Easing.linear,
useNativeDriver: true
}).start(() => this.state.spinAnim.setValue(0));
} else if (oldHeading - heading < -180) {
Animated.timing(
this.state.spinAnim,
{
toValue: 0,
duration: duration,
easing: Easing.linear,
useNativeDriver: true
}).start(() => this.state.spinAnim.setValue(360));
} else {
Animated.timing(
this.state.spinAnim,
{
toValue: heading,
duration: duration,
easing: Easing.linear,
useNativeDriver: true
}).start();
}
I had the same problem so my solution was to keep every time the exactly previous heading and if the distance is bigger than 180 i use two separate animations. One that animates from x to 0 and then sets value to 360 (or x to 360 and then sets value to 0) and the one that is the "default" and runs every time.
This is equivalent to avoiding rollbacks in CSS rotate transitions.
A way to do this is to recalculate the next angle based on the amplitude of the previous one. You still have to use interpolation, what changes is that the value fed to the animation will be the result of this function (credits), which adds the difference to the previous value and performs some modulo checks to guess the closest value:
function closestEquivalentAngle(from, to) {
var delta = ((((to - from) % 360) + 540) % 360) - 180;
return from + delta;
}
So for example, next time you move forward from 335 deg to 13 deg, the result will be 373, that is the original value plus a difference of 38 degrees. That works backwards as well. Just remember to store the value of the resulting angle somewhere, so you can use it on the next iteration as prevAngle.
Pulling it all together:
const RotateComp = (props) => {
const rotationValue = React.useRef(new Animated.Value(0)).current;
// Animation interpolation
const rotation = rotationValue.interpolate({
inputRange: [-360, 360],
outputRange: ['-360deg', '360deg'],
});
React.useEffect(() => {
// Obtain prevAngle and nextAngle, then...
Animated.timing(
rotationValue,
{
toValue: closestEquivalentAngle(prevAngle, nextAngle),
duration: 200,
useNativeDriver: true,
},
).start();
}, [rotationValue])
return (
// ...
<Animated.View
style={{
...props.style,
transform: [{rotate: rotation}],
}}
>
{props.children}
</Animated.View>
);
}

React Native Animated Loop start callback

I am struggling with React Native Animations here. The outcome is simple, I have an Animated.Image which I want to spin.
All good, until I want to loop thru the animation for n times and do something when it stops.
I have the following code .
Animated.loop(
Animated.timing(this.state.spin, {
toValue: 360,
duration: 1000,
easing: Easing.linear,
useNativeDriver: true,
}), {
iterations: 3
}
).start(() => {
console.log('done');
});
It spins 3 times as per loop iteration but no callback was fired when animation ends.
Here the Expo which replicates this: https://snack.expo.io/S1PjnfB9-
Try the code below
Animated.loop(
Animated.timing(this.state.spin, {
toValue: 360,
duration: 1000,
easing: Easing.linear,
useNativeDriver: true,
}), {
iterations: 3
}
).start(event => {
if (event.finished) {
console.log('finished');
}
});
I just added a check on the event response.
Hope it helps.
Found the answer.
It seems that if you take out useNativeDriver it works as it should be, and the callback is called.
This is weird...

Animated.spring finish event

I am trying to figure out when the Animated.spring event has completed so I can update some state.
I have tried this, but finish throws an undefined error here;
Animated.spring(
this.state.pan,
{ toValue: { x: 0, y: -500 } }
).start().finish(
this.setState({
carousel: false, hasNotification: false
})
);
Any other methods for dealing with this?
Thanks
According to the documentation
start takes a completion callback that will be called when the animation is done. If the animation is done because it finished running normally, the completion callback will be invoked with {finished: true}, but if the animation is done because stop was called on it before it could finish (e.g. because it was interrupted by a gesture or another animation), then it will receive {finished: false}.
So I think this should work:
Animated.spring(this.state.pan, { toValue: { x: 0, y: -500 } }).start(()=>{
this.setState({
carousel: false, hasNotification: false
})
});
You can pass an optional callback:
Animated.spring(
this.state.pan,
{ toValue: { x: 0, y: -500 } }
).start(this.stopAnimation.bind(this))
stopAnimation()
{
this.setState({
carousel: false, hasNotification: false
})
}