Set duration and delay when using withSpring in reanimated - react-native

I am very new to reanimated and have it working to a certain extent but I can not seem to add the duration of the animation. I would also like to add a delay but that is not as important as the duration. I am trying to change the opacity and its Y position. I can change the duration of the opacity but not the Y position. I have tried messing with the settings like damping, stiffness etc but that does not change the actual duration.
Am I using it in the wrong way?
I am currently trying this :
const offset = useSharedValue(400);
const imgOpacity= useSharedValue(0);
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateY: withSpring(offset.value, { mass: 0.1, damping: 100, stiffness: 100 }),
},
],
opacity: withTiming(imgOpacity.value, { duration: 1000 }),
};
});
I am changing the offset like this :
React.useEffect(() => {
if (show) {
offset.value = withSpring(10);
imgOpacity.value =1;
} else {
// alert("No Show")
}
}, [show]);
I have tried this to add withTiming but it is basically the same. Any advice would be appreciated.
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateY : withTiming(offset.value, {
duration: 1,
easing: Easing.bezier(0.25, 0.1, 0.25, 1),
}),
},
],
opacity: withTiming(imgOpacity.value, { duration: 1000 }),
};
});
The element I am trying to animate is this, is only has 3 images in it.
<Animated.View style={[animatedStyle]}>
<Image style={styles.BImage} source={{ uri: imgb }} />
<Image style={styles.AImage} source={{ uri: imga }} />
<Image style={styles.CImage} source={{ uri: imgc }} />
</Animated.View>

I have managed to achieve this but I am not sure this is the only and best way to do it. I added a config that is added to the Timing animation that adds a away to control the timing. I have also managed to add a delay before animation starts. I have added this in case anyone is starting out with reanimated and is having the same issue.
The code :
const offset = useSharedValue(90); // The offset is the Y value
const config = { // To control the lengh of the animation
duration: 500,
easing: Easing.bounce,
};
const animatedImg = useAnimatedStyle(() => { // The actual animation
return {
transform: [{ translateY: withTiming(offset.value, config) }],
};
});
<Animated.View style={[animatedImg2]}> // Object to animate
<Image
style={[styles.BImage]}
source={{
uri: 'https://image.tmdb.org/t/p/w440_and_h660_face/t6HIqrRAclMCA60NsSmeqe9RmNV.jpg',
}}
/>
</Animated.View>
The animated object can be moved by settting
offset.value = 200 // Method to change the Y value
Too add a delay before animation :
transform: [{ translateY: withDelay(1000, withTiming(offset.value, config)) }],

Related

React native shaking pin view when pin incorrect

i am trying to implement a feature when user enters the wrong password the 4 small circles shake to the right and left.
I created an animation component to test my code and it was working but now my problem is how do i apply it only when the password is incorrect?
Currently when i enter an incorrect password nothing happens. What i expect is for the view to move horizontally.
const shake = new Animated.Value(0.5);
const [subtle,setSubtle]=useState(true);
const [auto,setAuto]=useState(false);
const translateXAnim = shake.interpolate({
inputRange: [0, 1],
outputRange: [subtle ? -8 : -16, subtle ? 8 : 16],
});
const getAnimationStyles = () => ({
transform: [
{
translateX: translateXAnim,
},
],
});
const runAnimation = () => {
Animated.sequence([
Animated.timing(shake, {
delay: 300,
toValue: 1,
duration: subtle ? 300 : 200,
easing: Easing.out(Easing.sin),
useNativeDriver: true,
}),
Animated.timing(shake, {
toValue: 0,
duration: subtle ? 200 : 100,
easing: Easing.out(Easing.sin),
useNativeDriver: true,
}),
Animated.timing(shake, {
toValue: 1,
duration: subtle ? 200 : 100,
easing: Easing.out(Easing.sin),
useNativeDriver: true,
}),
Animated.timing(shake, {
toValue: 0,
duration: subtle ? 200 : 100,
easing: Easing.out(Easing.sin),
useNativeDriver: true,
}),
Animated.timing(shake, {
toValue: 0.5,
duration: subtle ? 300 : 200,
easing: Easing.out(Easing.sin),
useNativeDriver: true,
}),
]).start(() => {
if (auto) runAnimation();
});
};
const stopAnimation = () => {
shake.stopAnimation();
};
const handleConfirm = async()=>{
const result = await authApi();
if(!result.ok) {
setAuto(true)
setSubtle(true)
runAnimation()
stopAnimation()
return setLoginFailed(true);
}
setLoginFailed(false);
};
return(
<Animated.View style={[getAnimationStyles()]}>
<View style={styles.circleBlock}>
{
password.map(p=>{
let style =p != ''?styles.circleFill
: styles.circle
return <View style={style}></View>
})
}
</View>
</Animated.View>
issue is that you didn't use useRef() for
const shake = new Animated.Value(0.5);
should be
const shake = useRef(new Animated.Value(0.5)).current;
useRef returns a mutable ref object whose .current property is
initialized to the passed argument (initialValue). The returned object
will persist for the full lifetime of the component.
https://reactjs.org/docs/hooks-reference.html#useref
made separate expo snack with same input and output range values for animation shake effect. check it out.
Expo: https://snack.expo.io/#klakshman318/runshakeanimation

Victory Native Pie Chart Animation

I am trying to animate the changing of data in my Victory Native Pie Chart. I fetch the data from an API on a recurring timeout set up in the ComponentDidMount method. Once the data is retrieved I set it to a state variable which is passed to the data prop in the VictoryPie component with the animate props enabled as the docs show.
I am following this article and the Victory Chart docs for animations but mine does not behave in the same way as these examples.
Currently it sets the data correctly but without any smooth animation. It instantly jumps from initial state value to the fetched data value. Only time I see an animation is when the fetched data returns a zero value after the previous fetch had a value that wasn't zero.
export default class HaloXPChart extends Component {
constructor(props) {
super(props);
this.state = {
gamertag: this.props.gamertag ? this.props.gamertag : '',
dateRetrievalInterval: 1,
totalXp: 0,
startXp: 0,
spartanRank: 0,
xpChartData: [
{x: 'xp earned', y: 0},
{x: 'xp remaining', y: 1}
],
loading: false
};
}
componentDidMount() {
this.setState({loading: true});
this.recursiveGetData();
}
async recursiveGetData(){
this.setState({indicator: true}, async () => {
await this.getData()
this.setState({indicator: false});
let timeout = setTimeout(() => {
this.recursiveGetData();
}, this.state.dateRetrievalInterval * 60 * 1000);
});
}
async getData(){
await Promise.all([
fetch('https://www.haloapi.com/stats/h5/servicerecords/arena?players=' + this.state.gamertag, fetchInit),
fetch('https://www.haloapi.com/metadata/h5/metadata/spartan-ranks', fetchInit)
])
.then(([res1, res2]) => {
return Promise.all([res1, res2.json()])
}).then(([res1, res2) => {
const xp = res1.Results[0].Result.Xp;
const spartanRank = res1.Results[0].Result.SpartanRank;
this.setState({totalXp: xp});
const currentRank = res2.filter(r => r.id == this.state.spartanRank);
this.setState({startXp: currentRank[0].startXp});
this.setState({loading: false}, () => {
this.setState({xpChartData: [
{x: 'xp earned', y: this.state.totalXp - this.state.startXp},
{x: 'xp remaining', y: 15000000 - (this.state.totalXp - this.state.startXp)}
]});
});
})
.catch((error) => {
console.log(JSON.stringify(error));
})
}
render() {
return(
<View>
<Svg viewBox="0 0 400 400" >
<VictoryPie
standalone={false}
width={400} height={400}
data={this.state.xpChartData}
animate={{
easing: 'exp',
duration: 2000
}}
innerRadius={120} labelRadius={100}
style={{
labels: { display: 'none'},
data: {
fill: ({ datum }) =>
datum.x === 'xp earned' ? '#00f2fe' : 'black'
}
}}
/>
</Svg>
</View>
);
}
}
Try setting endAngle to 0 initially and set it to 360 when you load the data as described in this GitHub issue:
Note: Pie chart animates when it's data is changed. So set the data initially empty and set the actual data after a delay. If your data is coming from a service call it will do it already.
const PieChart = (props: Props) => {
const [data, setData] = useState<Data[]>([]);
const [endAngle, setEndAngle] = useState(0);
useEffect(() => {
setTimeout(() => {
setData(props.pieData);
setEndAngle(360);
}, 100);
}, []);
return (
<VictoryPie
animate={{
duration: 2000,
easing: "bounce"
}}
endAngle={endAngle}
colorScale={props.pieColors}
data={data}
height={height}
innerRadius={100}
labels={() => ""}
name={"pie"}
padding={0}
radius={({ datum, index }) => index === selectedIndex ? 130 : 120}
width={width}
/>
)

React Native spawn multiple Animated.View with a time delay

I have a Wave component that unmounts as soon as it goes out of bounds.
This is my result so far: https://streamable.com/hmjo6k
I would like to have multiple Waves spawn every 300ms, I've tried implementing this using setInterval inside useEffect, but nothing behaves like expected and app crashes
const Wave = ({ initialDiameter, onOutOfBounds }) => {
const scaleFactor = useRef(new Animated.Value(1)).current,
opacity = useRef(new Animated.Value(1)).current
useEffect(() => {
Animated.parallel(
[
Animated.timing(scaleFactor, {
toValue: 8,
duration: DURATION,
useNativeDriver: true
}),
Animated.timing(opacity, {
toValue: 0.3,
duration: DURATION,
useNativeDriver: true
})
]
).start(onOutOfBounds)
})
return (
<Animated.View
style={
{
opacity,
backgroundColor: 'white',
width: initialDiameter,
height: initialDiameter,
borderRadius: initialDiameter/2,
transform: [{scale: scaleFactor}]
}
}
/>
)
}
export default Wave
const INITIAL_WAVES_STATE = [
{ active: true },
]
const Waves = () => {
const [waves, setWaves] = useState(INITIAL_WAVES_STATE),
/* When out of bounds, a Wave will set itself to inactive
and a new one is registered in the state.
Instead, I'd like to spawn a new Wave every 500ms */
onWaveOutOfBounds = index => () => {
let newState = waves.slice()
newState[index].active = false
newState.push({ active: true })
setWaves(newState)
}
return (
<View style={style}>
{
waves.map(({ active }, index) => {
if (active) return (
<Wave
key={index}
initialDiameter={BUTTON_DIAMETER}
onOutOfBounds={onWaveOutOfBounds(index)}
/>
)
else return null
})
}
</View>
)
}
export default Waves
I think the useEffect cause the app crashed. Try to add dependencies for the useEffect
useEffect(() => {
Animated.parallel(
[
Animated.timing(scaleFactor, {
toValue: 8,
duration: DURATION,
useNativeDriver: true
}),
Animated.timing(opacity, {
toValue: 0.3,
duration: DURATION,
useNativeDriver: true
})
]
).start(onOutOfBounds)
}, [])

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.

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>
);
}