I am new to React Native animated, and I am trying to incorporate it into my app. I currently have an object that appears to fall from the middle to the bottom of the screen, using frequent state updates like this:
const [objHeight, setObjHeight] = useState((Dimensions.get("screen").height)/2)
useEffect(()=>{
if(objHeight > 0){
timerId = setInterval(()=>{
setObjHeight(objHeight => objHeight - 3)
}, 30) //30ms
return ()=>{
clearInterval(timerId)
}
},[objHeight])
//then the object looks like:
<View
style = {[{
position: "absolute",
backgroundColor: 'blue',
width: 50,
height: 60,
bottom: objHeight,
}]}>
</View>
I understand that this is an inefficient way to do animations on react, and that by using react animated we can make animate this on the UI thread instead. I have tried to replicate the above using react native animated.
const objHeight = new Animated.Value(screenHeight/2)
Animated.loop(
Animated.timing(objHeight, {
toValue: objHeight>0 ? objHeight - gravity : null,
duration: 3000,
useNativeDriver: true
}),
{iterations: 1000}
).start()
<Animated.View
style = {[{
backgroundColor: 'blue',
width: 50,
height: 60,
bottom: 200,
transform:[{translateY: objHeight}]
}]}>
</Animated.View>
However, the object does not move/animate. It just stays at the same height. What am I doing wrong? I find the documentation on react native animated is not particularly helpful.
Thank you
Snack Link
First create an Animated.Value with some initial value:
const bottomAnim = useRef(new Animated.Value(Dimensions.get('screen').height / 2)).current;
Then on component mount start the animation:
useEffect(() => {
Animated.timing(bottomAnim, {
toValue: 0,
duration: 3000,
useNativeDriver: true,
}).start();
}, []);
Finally, bind the animated value to the component:
return (
<Animated.View
style={[
{
position: 'absolute',
backgroundColor: 'blue',
width: 50,
height: 60,
bottom: bottomAnim,
},
]}/>
);
Related
I have a progress bar screen inside navigation that I need to be reset every time user clicks on that specific route. Now it only plays animation when I go to that route for the first time. My question is: how to reset barWidth so that animation would play every time user clicks on specific route?
What have I tried?
I thought that the problem is that the component is not re-rendering and thats why value is not resetting, but it looks like the problem is that the width of the bar doesn't reset when user clicks on the screen after animation plays.
At first I've tried using useRef hook and later changed to simply setting the animated value to 0, but in both cases the bar's width didn't reset.
Code:
const { width } = Dimensions.get("screen");
const SettingsScreen = ({navigation}) => {
const isFocused = useIsFocused();
return (
<FlatList
contentContainerStyle={style.barContainer}
data={[1, 2, 3, 4, 5]}
keyExtractor={(_, index) => index.toString()}
renderItem={() => (
<ProgressBar isFocused={isFocused} navigation={navigation} />
)}
></FlatList>
);
};
const ProgressBar = ({ navigation, isFocused }) => {
//const barWidth = React.useRef(new Animated.Value(0)).current;
const barWidth = new Animated.Value(0);
console.log(barWidth);
const finalWidth = width / 2;
React.useEffect(() => {
const listener = navigation.addListener("focus", () => {
Animated.spring(barWidth, {
toValue: finalWidth,
bounciness: 10,
speed: 2,
useNativeDriver: false,
}).start();
});
return listener;
}, [navigation]);
return (
<View style={style.contentContainer}>
<Animated.View style={[style.progressBar, { width: barWidth }]} />
</View>
);
};
const style = StyleSheet.create({
contentContainer: {
flex: 1,
justifyContent: "center",
padding: 30,
},
barContainer: {
padding: 30,
},
progressBar: {
backgroundColor: "green",
width: width / 2,
height: 15,
borderRadius: 15,
},
});
The addListener function is Deprecated. try to use addEventListener instead.
also, why is it inside a const listener with the return listener?
as i see it you can write the useEffect like that:
React.useEffect(() => {
navigation.addEventListener("focus", () => {
Animated.spring(barWidth, {
toValue: finalWidth,
bounciness: 10,
speed: 2,
useNativeDriver: false,
}).start();
});
}, [navigation]);
I am trying to make an animated menu components which consists out of a main menu button and a few menu items. I am trying to make the menu items to be animated in the following way (see gif below):
See below what i have tried so far but i am unsure of how to accomplish the desired effect. Since i dont know how many menu items there will be and thus cannot set predefined sizes for the container.
In my current attempt i do use predefined sizes and also have no control over each specific item's bounce and delay.
I am new to react native and definitely completely lost when it comes to animating layouts. Any help or pointers would be greatly appreciated!
(this is a part of a more exhaustive menu component hence the additional parent views, but i left most of that out to keep the question to the point).
menu component:
const Menu = function (props: any) {
const anim_def = { duration: 500, easing: Easing.out(Easing.exp) }
const animated_height = useSharedValue(60);
const animated_menu = useAnimatedStyle(() => {
return {
position: "absolute",
bottom: 20,
left: 20,
display: "flex",
flexDirection: "column",
overflow: "hidden",
height: withTiming(animated_height.value, anim_def),
}
});
const generate_menuitems = (): ReactNode[] | null => {
const items: ReactNode[] = [];
if (props.menu_items === undefined) return null;
props.menu_items.forEach((item, index) => {
const styles: ViewStyle = {
marginTop: (index === 0) ? 0 : 20,
backgroundColor: item.backgroundColor
}
items.push(<ImageButton
image={item.image ?? 0}
onPress={() => { on_press_behaviour(index, item); }}
underlayColor={item.highlightColor}
style={[style.buttons, props.style?.buttons, styles]}
></ImageButton>)
});
return items;
}
const on_open_menu = () => {
animated_height.value = 300;
}
return (
<View pointerEvents="box-none" style={{ ...style.container, ...props.style?.container }}>
<View pointerEvents="box-none" style={style.container_wrapper}>
<Animated.View style={animated_menu}>
<ImageButton onPress={on_open_menu} onLayout={(l: LayoutChangeEvent) => {console.log(l.nativeEvent.layout)}} image={0} underlayColor="orange" style={[style.buttons, props.style?.buttons, { position: "absolute", left: 0, bottom: 0, zIndex: 2 }]}></ImageButton>
{generate_menuitems()}
</Animated.View>
</View>
</View>
);
}
component styles:
const style = StyleSheet.create({
container: {
position: "absolute",
left: 0,
bottom: 0,
width: "100%",
height: "100%",
},
container_wrapper: {
position: "relative",
width: "100%",
height: "100%",
},
buttons: {
width: 60,
aspectRatio: 1,
borderRadius: 30,
backgroundColor: "green",
marginTop: 20
}
});
In my react native app I want to have a progressbar animated values, problem is when component is mounted the bar is full already (View with backgroundColor RED is visible) even though progressValue is 0 according to my console logs... if I execute onStart nothing moves since it appears like animation already finished...
Is it because I'm using translate wrongly? How can I achieve correct animated effect?
const width = Math.round(Dimensions.get('window').width)-30;
const progressValue = new Animated.Value(0);
//const animation = null;
const onStart = (duration ) =>
{
console.log(duration,width);
const animation = Animated.timing(progressValue, {
duration: duration,
toValue: width,
easing: Easing.linear,
useNativeDriver: true,
}).start();
};
<View style={{width: width, height: 4,backgroundColor: 'white'}}>
<Animated.View style={{width: width, height: 4, backgroundColor: 'red', transform: [{translateX: progressValue}]}} />
</View>
two things which i can see, always useRef for animation values
const width = Math.round(Dimensions.get('window').width)-30;
const progressValue = useRef(new Animated.Value(0)).current; // add this
//const animation = null;
const onStart = useCallback((duration ) =>
{
console.log(duration,width);
const animation = Animated.timing(progressValue, {
duration: duration,
toValue: width,
easing: Easing.linear,
useNativeDriver: true,
}).start();
},[progressValue]);
<View style={{width: width, height: 4,backgroundColor: 'white'}}>
<Animated.View style={{width: width, height: 4, backgroundColor: 'red', transform: [{translateX: progressValue}]}} />
</View>
Also im hoping youre calling onStart in a useEffect like this
useEffect(() => {
onStart()
},[onStart])
Hope it helps. feel free for doubts
I only encountered this issue once I incorporated the useEffect() hook as suggested by React native - "this.setState is not a function" trying to animate background color?
With the following, I get
Rendered more hooks than during the previous render
export default props => {
let [fontsLoaded] = useFonts({
'Inter-SemiBoldItalic': 'https://rsms.me/inter/font-files/Inter-SemiBoldItalic.otf?v=3.12',
'SequelSans-RomanDisp' : require('./assets/fonts/SequelSans-RomanDisp.ttf'),
'SequelSans-BoldDisp' : require('./assets/fonts/SequelSans-BoldDisp.ttf'),
'SequelSans-BlackDisp' : require('./assets/fonts/SequelSans-BlackDisp.ttf'),
});
if (!fontsLoaded) {
return <AppLoading />;
} else {
//Set states
const [backgroundColor, setBackgroundColor] = useState(new Animated.Value(0));
useEffect(() => {
setBackgroundColor(new Animated.Value(0));
}, []); // this will be only called on initial mounting of component,
// so you can change this as your requirement maybe move this in a function which will be called,
// you can't directly call setState/useState in render otherwise it will go in a infinite loop.
useEffect(() => {
Animated.timing(this.state.backgroundColor, {
toValue: 100,
duration: 5000
}).start();
}, [backgroundColor]);
var color = this.state.colorValue.interpolate({
inputRange: [0, 300],
outputRange: ['rgba(255, 0, 0, 1)', 'rgba(0, 255, 0, 1)']
});
const styles = StyleSheet.create({
container: { flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: color
},
textWrapper: {
height: hp('70%'), // 70% of height device screen
width: wp('80%'), // 80% of width device screen
backgroundColor: '#fff',
justifyContent: 'center',
alignItems: 'center',
},
myText: {
fontSize: hp('2%'), // End result looks like the provided UI mockup
fontFamily: 'SequelSans-BoldDisp'
}
});
return (
<Animated.View style={styles.container}>
<View style={styles.textWrapper}>
<Text style={styles.myText}>Login</Text>
</View>
</Animated.View>
);
}
};
Im just trying to animate fade the background color of a view. I tried deleting the first useEffect in case it was causing some redundancy, but that did nothing. Im new to ReactNative - what is wrong here?
EDIT:
export default props => {
let [fontsLoaded] = useFonts({
'Inter-SemiBoldItalic': 'https://rsms.me/inter/font-files/Inter-SemiBoldItalic.otf?v=3.12',
'SequelSans-RomanDisp' : require('./assets/fonts/SequelSans-RomanDisp.ttf'),
'SequelSans-BoldDisp' : require('./assets/fonts/SequelSans-BoldDisp.ttf'),
'SequelSans-BlackDisp' : require('./assets/fonts/SequelSans-BlackDisp.ttf'),
});
//Set states
const [backgroundColor, setBackgroundColor] = useState(new Animated.Value(0));
useEffect(() => {
setBackgroundColor(new Animated.Value(0));
}, []); // this will be only called on initial mounting of component,
// so you can change this as your requirement maybe move this in a function which will be called,
// you can't directly call setState/useState in render otherwise it will go in a infinite loop.
useEffect(() => {
Animated.timing(useState(backgroundColor), {
toValue: 100,
duration: 7000
}).start();
}, [backgroundColor]);
// var color = this.state.colorValue.interpolate({
// inputRange: [0, 300],
// outputRange: ['rgba(255, 0, 0, 1)', 'rgba(0, 255, 0, 1)']
// });
//------------------------------------------------------------------->
if (!fontsLoaded) {
return <AppLoading />;
} else {
const styles = StyleSheet.create({
container: { flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: backgroundColor
New errors:
invalid prop 'color' supplied to 'Stylesheet'
Animated useNativeDriver was not specified
On your first render (I'm guessing) only the useFonts hook will be called as you return <AppLoading /> since !fontsLoaded. The rest of your hooks are in the else block, meaning you won't have the same number of hooks on every render.
Check out https://reactjs.org/docs/hooks-rules.html for more explanation, especially https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
The useNativeDriver error exists because you didn't specify it:
Your code:
useEffect(() => {
Animated.timing(useState(backgroundColor), {
toValue: 100,
duration: 7000
}).start();
Fix:
useEffect(() => {
Animated.timing(useState(backgroundColor), {
toValue: 100,
duration: 7000,
useNativeDrive: true
}).start();
Hope this helps!
I am trying to render multiple components inside of a parent component on a specific position (based on some calculations). The calculations that give me the vertical position look correct, but the components are not displayed in the position they should. I have tried both absolute window position and relative component position with no luck.
The parent looks like follows:
const top = 170;
const bottom = 10;
const left = 10;
const right = 10;
const styles = StyleSheet.create({
grid: {
flex: 1,
position: 'absolute',
top: top,
height: Dimensions.get('window').height - top - bottom,
width: Dimensions.get('window').width - left - right,
borderLeftColor: 'black',
borderLeftWidth: 1,
borderBottomColor: 'black',
borderBottomWidth: 1
}
});
const DrawGrid: React.FC<IDrawGrid> = ({ maxDistance, elements }) => {
const [gridSize, setGridSize] = useState<LayoutRectangle>();
return (
<View style={styles.grid} onLayout={(event) => {
setGridSize(event.nativeEvent.layout);
}}>
{elements.map((element, index) => {
return (
<DrawElement element={element} maxDistance={maxDistance} gridSize={gridSize} index={index * 2} />
)
})}
</View>
);
};
And the child component that renders all the elements looks like follows:
const top = 170;
const bottom = 20;
const left = 10;
const right = 10;
const styles = StyleSheet.create({
elementContainer: {
borderLeftColor: 'red',
borderLeftWidth: 1,
borderTopColor: 'red',
borderTopWidth: 1,
borderRightColor: 'red',
borderRightWidth: 1,
borderBottomColor: 'red',
borderBottomWidth: 1,
borderRadius: 5,
padding: 2,
position: 'relative',
alignSelf: 'flex-start'
}
});
const getVerticalPosition = (someDistance: number, maxDistance: number, height: number) => {
if (!someDistance || !maxDistance) return { top: 0 };
const topDistance = (1 - (someDistance / maxDistance)) * height;
return { top: topDistance };
};
const DrawElement: React.FC<IDrawElement> = ({ maxDistance, element, gridSize, index }) => {
const styleVertical = getVerticalPosition(someDistance, maxDistance, gridSize.height);
return (
<View key={key} style={[styles.elementContainer, styleVertical]}>
<Text>top: {styleVertical.top.toFixed(2)}</Text>
</View>
);
};
I can see how getVerticalPosition returns the right value, but the element is never located in the position expected. In the snapshot below I am printing the top value for each element, and we can see it is not respected at all. (horizontal location is out of the scope of the problem)
My first thought was that I am messing up the styles somehow, I also tried giving a different zindex to each element without luck. Any ideas what could happen? Any help would be much appreciated, thank you!
I think you don't understand how the layout type works in React Native. Here is a link: https://reactnative.dev/docs/flexbox#absolute--relative-layout. The main problem is relative position for each child element related to its own initial position.
By default, all elements in React Native have position equal to relative, so you don't need to put it in style definition.
To better understanding relative position scheme, I suggest you consider its rendering process in two steps:
rendering children without any of top, left, bottom, right properties;
shifting children according to their top, left, bottom, right properties from their current positions;
Imagine that you have such code:
<View style={styles.parent}>
<View style={styles.firstChild}/>
<View style={styles.secondChild}/>
</View>
const styles = StyleSheet.create({
parent: {
top: 100,
flex: 1,
},
firstChild: {
top: 100,
height: 50,
},
secondChild: {
top: 80,
height: 70,
},
});
If we apply two steps mentioned above to this example it will look like this:
To solve your problem you need to apply absolute position scheme to children. Only absolute position scheme place element, with for example top: 100 property, to 100 points down related to the current top of its parent.
I hope it will help you a little.