Looping a react native animated animation - react-native

I am attempting to put the following animation in an infinite loop until a particular state occurs:
class MyModal extends Component {
constructor() {
super()
this.springValue = new Animated.Value(0.3)
}
spring = () => {
this.springValue.setValue(0.3)
Animated.spring(
this.springValue,
{
toValue: 1,
friction: 1,
tension: 1,
duration:5000
}
).start()
}
componentDidMount() {
this.spring()
}
render() {
return (
<View>
<Modal
animationType="none"
transparent={false}
visible={this.state.modalVisible}
onRequestClose={() => null}
>
<View style={styles.backgroundStyle}>
<Animated.Image
source={require("./my-awesome-image.png")}
style={{ width: 143, height: 125, top: 100, transform: [{scale: this.springValue}]}}
/>
</View>
</Modal>
</View>
);
}
}
Everything here works great, the animation completes once (as I'm not looping anywhere).
How do I keep my Animated.Image looping until I reach a particular state? I just want it infinite looping and the ability to either stop the animation or start another animation when I'm ready to.

Store your animation in a variable you can access and just wrap your animation with Animated.loop(). Then you can freely use .start() and .stop() on that variable holding the animation as you please.
So something like this should do:
this.springAnimation = Animated.loop(
Animated.spring(
this.springValue,
{
toValue: 1,
friction: 1,
tension: 1,
duration:5000
}
).start()
)
You can find more information about that here:
https://facebook.github.io/react-native/docs/animated.html#loop

Pass a callback to the start function to check whether to restart the animation. You could break it down to something like this:
onSpringCompletion = () => {
if (arbitraryCondition) {
this.spring();
}
}
spring = () => {
this.springValue.setValue(0.3)
Animated.spring(
this.springValue,
{
toValue: 1,
friction: 1,
tension: 1,
duration:5000
}
).start(this.onSpringCompletion);
}

You can use setInterval to keep the animation playing and remove the interval whenever you want. I would do this:
componentDidMount() {
this.interval = setInterval(() => this.spring(), 5000) // Set this time higher than your animation duration
}
...
// Some where in your code that changes the state
clearInterval(this.interval)
...

Related

How to repeat the last few frames of animation for React Animation

I have an app which the user holds down the screen and an animation starts to play. This is not a normal animation. It uses multiple frames to create this animation much like a cartoon from Disney :)
The animation plays from the start to the end and loops. But I want it to loop over the last 4 or 5 frames repeatedly. At the moment it doesn't do this.
Here is the code:
Initialisation in the constructor:
this.animations = new Animated.Value(0);
this.opacity = [];
Images.map((item, index) => {
this.opacity.push(
this.animations.interpolate({
inputRange: [index - 1, index, index + 1],
outputRange: [0, 100, 0],
}),
)
})
The animation function and the hold screen function (for what it's worth):
startAnimation = () => {
Animated.loop(
Animated.timing(this.animations, {
toValue: length - 1,
duration: 50 * length,
easing: Easing.linear,
useNativeDriver: true,
})
).start();
}
onItemMouseDown = () => {
this.startAnimation()
this.setState({
isOn: true,
pauseToggle: 'down',
mouseUp: 'no',
twoSecOver: false,
})
The render:
{this.state.isOn === true ? (
<View style={styles.container}>
{Images.map((item, index) => {
const opacity = this.opacity[index];
return (
<Animated.View
key={item.id}
style={[styles.anim, { animations: item, opacity}]}
>
<Image source={item.source} style={styles.animSize}/>
<Text style={styles.timer}>Timer:{ms(this.state.time,{verbose: true})}</Text>
</Animated.View>
);
})}
</View>
) : null}
I've looked at this: Looping over the last few entries of an array
and this:
using array.map with repeat in Javascript
but I'm doomed to failure, so need some help.
I've been advised to call:
Images.slice(startIndex, endIndex).map instead of just .map? Set startIndex to Images.length - 6 and endIndex to Images.length initially, then Images.length-6 and images.length afterwards. This actually plays the last few frames on a loop but doesn't play the first 18 frames so isn't useful! I don't know how to get it to map through the first 28 frames, then start the loop through the last 6.
How would I incorporate that into the render?
I've also tried using Animated.Sequence (https://reactnative.dev/docs/animations#tracking-dynamic-values) but have no idea how to do it.
T
You could try this:
Define a state for your images stateImages if you have already have the images you can define in the constructor:
this.state = {
stateImages: Images,
}
Remove the loop onstartAnimation and set a callback on the completion to start the loop
startAnimation = () => {
Animated.timing(this.animations, {
toValue: length - 1,
duration: 50 * length,
easing: Easing.linear,
useNativeDriver: true,
}).start(({ finished }) => {
// completion callback
// if the user have the mouse down. You should track the current state because
// you want the animation if the user stop the action
if (isMouseDown) {
startLoopAnimation();
}
});
}
// The animation loop function
startLoopAnimation = () => {
this.animations = new Animated.Value(0);
this.opacity = [];
Images.map((item, index) => {
this.opacity.push(
this.animations.interpolate({
inputRange: [index - 1, index, index + 1],
outputRange: [0, 100, 0],
}),
)
})
let orderedImages = Images.slice(Images.length - 6, Images.length);
this.setState({
stateImages: orderedImages,
});
Animated.timing(this.animations, {
toValue: length - 1,
duration: 50 * length,
easing: Easing.linear,
useNativeDriver: true,
}).start(({ finished }) => {
// completion callback
// if the user have the mouse down. You should track the current state because
// you want the animation stop if the user stop the action
if (isMouseDown) {
startLoopAnimation();
}
});
}
Your render should be almost the same, but the Images map is processed with the stateImages that holds your current portion of the loop.
{this.state.isOn === true ? (
<View style={styles.container}>
{this.state.stateImages.map((item, index) => {
const opacity = this.opacity[index];
return (
<Animated.View
key={item.id}
style={[styles.anim, { animations: item, opacity}]}
>
<Image source={item.source} style={styles.animSize}/>
<Text style={styles.timer}>Timer:{ms(this.state.time,{verbose: true})}</Text>
</Animated.View>
);
})}
</View>
) : null}
When the the user stop the animation action, you must restart your stateImages:
this.setState({
stateImages: Images,
});
The code is untested but this is the idea.

Toggling the animated value in order to fade in/out the view

I'm trying to toggle view's opacity with animated value, by handling the button click, but I'm not getting the desired result, except the first time button is clicked, it fades out (opacity = 0) but when I press the button again nothing happens and I can't see my view. Here's the code:
export default class App extends React.Component {
state = {
animation: new Animated.Value(1)
}
startAnimation = () => {
const { animation } = this.state
Animated.timing(animation, {
toValue: animation === 0 ? 1 : 0,
duration: 1000
}).start()
}
render() {
const animatedStyle = {
opacity: this.state.animation
}
return (
<View style={styles.container}>
<Animated.View style={[styles.box, animatedStyle]} />
<Button title="Toggle fade" onPress={this.startAnimation} />
</View>
);
}
} .
Does anybody know what am I doing (understanding) wrong?
Thanks!
I think it is because you don't change the state for your animated values, and this const { animation } = this.state will have always the same value, and toValue: animation === 0 ? 1 : 0, will have the same value too. I try to show you how I did this in my projects, but you have to update it for your needs.
export default class App extends React.Component {
state = {
animation: new Animated.Value(1),
isVisible: false //add a new value to check your state
}
startAnimation = () => {
const { isVisible } = this.state
Animated.timing(animation, {
toValue: isVisible === 0 ? 1 : 0,
duration: 1000
}).start(() => {
this.setState({ isVisible: !this.state.isVisible });//set the new state, so the next click will have different value
})
}
render() {
const animatedStyle = {
opacity: this.state.animation
}
return (
<View style={styles.container}>
<Animated.View style={[styles.box, animatedStyle]} />
<Button title="Toggle fade" onPress={this.startAnimation} />
</View>
);
}
} .
I am using this:
let val = this.state.sliderOpen ? 0.8 : 0;
Animated.timing( // Animate over time
this.state.sliderAnimation, // The animated value to drive
{
toValue: val, // Animate to opacity: 1 (opaque)
duration: 5, // Make it take a while
}
).start();
this.setState({
sliderOpen : !this.state.sliderOpen
})
Maybe try to extract the value to be changed.
Thanks to #oma I was able to get it work, here's the snack:
Toggle opacity in React Native
Besides that, I've found a nice article on this where this feature can be reused:
Animating appearance and disappearance in React Native
And here's the snack of the working example, with slight modification.
Animate opacity
This solution looks pretty well, hope you can benefit from it.
I made a node package react-native-fade-in-out that toggles a view's opacity with an animated value. You can look at the source code to see how it is accomplished, but here's a simplified version:
import React, {PureComponent} from 'react';
import {Animated} from 'react-native';
export default class FadeInOut extends PureComponent {
state = {
fadeAnim: new Animated.Value(this.props.visible ? 1 : 0),
};
componentDidUpdate(prevProps) {
if (prevProps.visible !== this.props.visible) {
Animated.timing(this.state.fadeAnim, {
toValue: prevProps.visible ? 0 : 1,
duration: 300,
}).start();
}
}
render() {
return (
<Animated.View style={{...this.props.style, opacity: this.state.fadeAnim}}>
{this.props.children}
</Animated.View>
);
}
}

React native animation is not working for the second time

I have used the default "Animated" package with react native for my animations in the application. Animations in the following code is working fine. But when I navigate to another page and come back to this screen the animation is not working. Once the page gets loaded from ground level only it is working again. What could be the reason ? Can someone please help me to sort this out.
class LoginScreen extends Component {
static navigationOptions = {
header: null
}
state = {
username: '',
password: '',
animation: {
usernamePostionLeft: new Animated.Value(795),
passwordPositionLeft: new Animated.Value(905),
loginPositionTop: new Animated.Value(1402),
statusPositionTop: new Animated.Value(1542)
}
}
navigateToScreen = link => event => {
this.props.navigation.navigate(link)
}
componentDidMount() {
const timing = Animated.timing
Animated.parallel([
timing(this.state.animation.usernamePostionLeft, {
toValue: 0,
duration: 1700
}),
timing(this.state.animation.passwordPositionLeft, {
toValue: 0,
duration: 900
}),
timing(this.state.animation.loginPositionTop, {
toValue: 0,
duration: 700
}),
timing(this.state.animation.statusPositionTop, {
toValue: 0,
duration: 700
})
]).start()
}
render() {
return (
<View style={styles.container}>
<ImageBackground
source={lem_bg}
blurRadius={10}
style={styles.imageBgContainer}>
<View style={styles.internalContainer}>
<Animated.View style={{position: 'relative', top:
this.state.animation.usernamePostionLeft, width: '100%'}}>
<Text style={styles.LEMHeader}>LEM<Text style={styles.followingtext}>mobile</Text></Text>
</Animated.View>
</ImageBackground>
</View>
....MORE JSX ARE THERE...
)
}
}
componentDidMount() won't call when you navigate back from another screen. for this, you have to create your own callback method for performing this animation when you pop() from another screen. Consider below code change
first screen
navigateToScreen = link => event => {
this.props.navigation.navigate(link,{
callback:this.runAnimation
})
}
componentDidMount() {
this.runAnimation()
}
runAnimation(){
const timing = Animated.timing
Animated.parallel([
timing(this.state.animation.usernamePostionLeft, {
toValue: 0,
duration: 1700
}),
timing(this.state.animation.passwordPositionLeft, {
toValue: 0,
duration: 900
}),
timing(this.state.animation.loginPositionTop, {
toValue: 0,
duration: 700
}),
timing(this.state.animation.statusPositionTop, {
toValue: 0,
duration: 700
})
]).start()
}
on the second screen when you pop() navigation to back, call this callback
this.props.navigator.pop()
this.props.callback()

How to add a function call to Animated.sequence in react native

I have a button at the middle of my screen. onScroll I want the button to scale down to 0 to disappear and then scale back up to 1 to reappear in a new position at the bottom of the screen. I want to be able call setState (which controls the position of the button) in between the scale down and scale up animations. Something like the code below. Any idea of the best way to add a function call in between these two animations? Or an even better way of doing this?
animateScale = () => {
return (
Animated.sequence([
Animated.timing(
this.state.scale,
{
toValue: 0,
duration: 300
}
),
this.setState({ positionBottom: true }),
Animated.timing(
this.state.scale,
{
toValue: 1,
duration: 300
}
)
]).start()
)
}
After more research I found the answer.start() takes a callback function as shown here:
Calling function after Animate.spring has finished
Here was my final solution:
export default class MyAnimatedScreen extends PureComponent {
state = {
scale: new Animated.Value(1),
positionUp: true,
animating: false,
};
animationStep = (toValue, callback) => () =>
Animated.timing(this.state.scale, {
toValue,
duration: 200,
}).start(callback);
beginAnimation = (value) => {
if (this.state.animating) return;
this.setState(
{ animating: true },
this.animationStep(0, () => {
this.setState(
{ positionUp: value, animating: false },
this.animationStep(1)
);
})
);
};
handleScrollAnim = ({ nativeEvent }) => {
const { y } = nativeEvent.contentOffset;
if (y < 10) {
if (!this.state.positionUp) {
this.beginAnimation(true);
}
} else if (this.state.positionUp) {
this.beginAnimation(false);
}
};
render() {
return (
<View>
<Animated.View
style={[
styles.buttonWrapper,
{ transform: [{ scale: this.state.scale }] },
this.state.positionUp
? styles.buttonAlignTop
: styles.buttonAlignBottom,
]}
>
<ButtonCircle />
</Animated.View>
<ScrollView onScroll={this.handleScrollAnim}>
// scroll stuff here
</ScrollView>
</View>
);
}
}
That is correct answer.
Tested on Android react-native#0.63.2
Animated.sequence([
Animated.timing(someParam, {...}),
{
start: cb => {
//Do something
console.log(`I'm wored!!!`)
cb({ finished: true })
}
},
Animated.timing(someOtherParam, {...}),
]).start();

Animate listview items when they are added/removed from datasource

Can someone give me an idea of how this can be done, e.g. animate the height from 0 when added and back to 0 when removed?
Animation when added is easy, just use Animated in componentDidMount with your listRow , for example:
componentDidMount = ()=> {
Animated.timing(this.state._rowOpacity, {
toValue: 1,
duration: 250,
}).start()
}
Animate a component before unmount is much harder in react-native. You should set a handler for ListView. When dataSource changed, diff the data, start Animated to hide removed row, and set new dataSource for ListView.
Here you can get full working example for opacity animation:
import React from 'react-native';
export default class Cell extends React.Component {
constructor(props) {
super(props);
this.state = {
opacity: new React.Animated.Value(0)
};
}
componentDidMount() {
React.Animated.timing(this.state.opacity, {
toValue: 1,
duration: 250,
}).start();
}
render() {
return (
<React.Animated.View style={[styles.wrapper, {opacity: this.state.opacity}]}>
<React.Image source={{uri: 'http://placehold.it/150x150'}} style={styles.image}/>
<React.Text style={styles.text}>
Text
</React.Text>
</React.Animated.View>
);
}
}
const styles = React.StyleSheet.create({
wrapper: {
flex: 1,
flexDirection: 'row',
justifyContent: 'flex-start',
alignItems: 'center',
},
image: {
height: 40,
width: 40,
marginRight: 16,
backgroundColor: '#C9D5E6'
},
text: {
fontSize: 20
}
});
In case you need for removing an item from the list, here's how to do the ListRow component:
class DynamicListRow extends Component {
// these values will need to be fixed either within the component or sent through props
_defaultHeightValue = 60;
_defaultTransition = 500;
state = {
_rowHeight : new Animated.Value(this._defaultHeightValue),
_rowOpacity : new Animated.Value(0)
};
componentDidMount() {
Animated.timing(this.state._rowOpacity, {
toValue : 1,
duration : this._defaultTransition
}).start()
}
componentWillReceiveProps(nextProps) {
if (nextProps.remove) {
this.onRemoving(nextProps.onRemoving);
} else {
// we need this for iOS because iOS does not reset list row style properties
this.resetHeight()
}
}
onRemoving(callback) {
Animated.timing(this.state._rowHeight, {
toValue : 0,
duration : this._defaultTransition
}).start(callback);
}
resetHeight() {
Animated.timing(this.state._rowHeight, {
toValue : this._defaultHeightValue,
duration : 0
}).start();
}
render() {
return (
<Animated.View
style={{height: this.state._rowHeight, opacity: this.state._rowOpacity}}>
{this.props.children}
</Animated.View>
);
}
}
i've posted a complete tutorial to this question in this blog post. And it's explaining step by step what you need to do to accomplish both adding and removing an item and animate this process.
For adding is pretty straight forward, but for removing looks like it's a little bit more complex.
http://moduscreate.com/react-native-dynamic-animated-lists/
Here's a full example for height and opacity animation. It supports both adding and removing an element. The key point is that you need to reset the height and opacity after the disappearing animation completes. Then you immediately delete the item from the source.
export const ListItem = (props: ListItemProps) => {
// Start the opacity at 0
const [fadeAnim] = useState(new Animated.Value(0));
// Start the height at 0
const [heightAnim] = useState(new Animated.Value(0));
/**
* Helper function for animating the item
* #param appear - whether the animation should cause the item to appear or disappear
* #param delay - how long the animation should last (ms)
* #param callback - callback to be called when the animation finishes
*/
const _animateItem = (appear: boolean = true, delay: number = 300, callback: () => void = () => null) => {
Animated.parallel([
Animated.timing(
fadeAnim,
{
toValue: appear ? 1 : 0,
duration: delay,
}
),
Animated.timing(
heightAnim,
{
toValue: appear ? 100 : 0,
duration: delay,
}
),
]).start(callback);
};
// Animate the appearance of the item appearing the first time it loads
// Empty array in useEffect results in this only occuring on the first render
React.useEffect(() => {
_animateItem();
}, []);
// Reset an item to its original height and opacity
// Takes a callback to be called once the reset finishes
// The reset will take 0 seconds and then immediately call the callback.
const _reset = (callback: () => void) => {
_animateItem(true,0, callback);
}
// Deletes an item from the list. Follows the following order:
// 1) Animate the item disappearing. On completion:
// 2) Reset the item to its original display height (in 0 seconds). On completion:
// 3) Call the parent to let it know to remove the item from the list
const _delete = () => {
_animateItem(false, 200, () => _reset(props.delete));
};
return (
<Animated.View
style={{height: heightAnim, opacity: fadeAnim, flexDirection: 'row'}}>
<Text>{props.text}</Text>
<Button onPress={() => _delete()}><Text>Delete</Text></Button>
</Animated.View>
);
}