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
}
});
Related
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,
},
]}/>
);
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.
I am studying typescript in react native.
If I typed the following code as vanilla javascript, the app works well.
But typed as a typescript, I received a message circleStyles.push(styles.circleCorrect); is an error. I understand it seems like the problem of type but I do not get how to solve it.
Does anyone have an idea?
import React from "react";
import { View, StyleSheet, Dimensions, Image } from "react-native";
const screen = Dimensions.get("window");
const styles = StyleSheet.create({
container: {
position: "absolute",
top: 0,
bottom: 0,
left: 0,
right: 0,
flex: 1,
alignItems: "center",
justifyContent: "center"
},
circle: {
backgroundColor: "#ff4136",
width: screen.width / 2,
height: screen.width / 2,
borderRadius: screen.width / 2,
alignItems: "center",
justifyContent: "center"
},
circleCorrect: {
backgroundColor: "#28A125"
},
icon: {
width: screen.width / 3
}
});
export const Alert = ({ correct, visible }) => {
if (!visible) return null;
const icon = correct
? require("../assets/check.png")
: require("../assets/close.png");
const circleStyles = [styles.circle];
if (correct) {
**circleStyles.push(styles.circleCorrect);**
}
return (
<View style={styles.container}>
<View style={circleStyles}>
<Image source={icon} style={styles.icon} resizeMode="contain" />
</View>
</View>
)
}
This is because TypeScript uses inference to implicitly determine types so circleStyles type is evaluated as an array of objects having the same fields than styles.circle.
You can declare circleStyles as an array of any kind of value.
const circleStyles: any[] = [styles.circle];
I would advice you however to type it more precisely by creating an interface with optional fields or using union types....
I have a background image for my component. I want 1 more image which is 48 * 48 (small image)
to move on it from left to right and top randomly. Here is the code of my background image -
const sectionStyle = {
width: "100vw",
height: "100vh",
backgroundImage: `url(${Background})`,
backgroundSize: 'cover',
backgroundPosition: 'center'
};
Then the render of the compoent has div which uses sectionStyle.
How do I get this small image moving on the background image randomly every few seconds?
One way to accomplish this would be to use use absolute positioning and define state for position of your image. Then you can change positions with setInterval.
It might not be the best way possible but it will get the work done. Here's an example implementation. (I've taken random values for the ease of understanding) -
const styles = {
mainContainerStyle: {
margin: '50px',
background: 'yellow',
padding: '20px',
height: '600px',
},
backgroundContainer: {
position: 'absolute',
background: `url('YOUR_BACKGROUND_IMAGE')`,
width: '400px',
height: '400px',
},
imageStyle: {
position: 'absolute',
top: 0,
left: 0,
width: 45,
height: 45,
borderRadius: 5,
},
}
class ExampleComponent extends Component {
constructor(props) {
super(props);
this.state = {
left: 0,
top: 0,
}
}
randomMovement = () => {
this.setState((prevState) => ({
left: prevState.left + 30 * Math.random(),
top: prevState.top + 30 * Math.random(),
}));
}
render() {
return (
<div style={styles.mainContainerStyle}>
<div style={styles.backgroundContainer}>
<img
src="YOUR_IMAGE_URL"
style={{
...styles.imageStyle,
top: this.state.top,
left: this.state.left,
}}
/>
</div>
</div>
);
}
componentDidMount() {
this.startMovement = setInterval(this.randomMovement, 3000);
}
}
Hope it helps.
Every time I press a button the progress bar will go up %20 but the problem is for instance if its at %80 and I reload the app it will show it as %60 instead, I was wondering if anyone could help me fix this issue to have the progress bars percentage stay the same after reloading or closing the app.
'use strict';
var React = require('react-native');
var ProgressBar = require('react-native-progress-bar');
var {
AppRegistry,
AsyncStorage,
StyleSheet,
Text,
View,
TouchableHighlight
} = React;
var styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#FFF',
},
button: {
alignSelf: 'center',
marginTop: 50,
width: 100,
height: 50,
backgroundColor: '#0059FF',
borderRadius: 8,
borderWidth: 2,
borderColor: '#0059FF'
},
buttonClear: {
alignSelf: 'center',
marginTop: 10,
width: 100,
height: 50,
backgroundColor: '#3B3A3A',
borderRadius: 8,
borderWidth: 2,
borderColor: '#3B3A3A'
},
buttonText: {
fontSize: 18,
textAlign: 'center',
lineHeight: 33,
color: '#FFF',
}
});
var PROGRESS = 0;
class BasicStorageExample extends React.Component {
constructor(props) {
super(props);
this.state = {
progress: PROGRESS
}
}
componentDidMount() {
AsyncStorage.getItem('progressbar')
.then((value) => {
JSON.parse(value);
this.setState({
progress: value
});
console.log('Progress on load: ' + value);
})
.done();
}
onButtonPress() {
AsyncStorage.setItem('progressbar', JSON.stringify(PROGRESS))
.then(() => {
JSON.parse(PROGRESS);
this.setState({
progress: PROGRESS += 0.2
});
console.log('Progress on Button Press: ' + PROGRESS);
})
.done();
}
onButtonClearPress() {
AsyncStorage.setItem('progressbar', JSON.stringify(PROGRESS))
.then(() => {
JSON.parse(PROGRESS);
PROGRESS = 0;
this.setState({
progress: 0
});
})
.done();
}
render() {
return (
<View style={styles.container}>
<ProgressBar
fillStyle={{}}
backgroundStyle={{backgroundColor: '#cccccc', borderRadius: 2}}
style={{marginTop: 10, width: 300}}
progress={this.state.progress} />
<TouchableHighlight
ref="button"
style={styles.button}
underlayColor='#002C7F'
onPress={this.onButtonPress.bind(this)}>
<Text style={styles.buttonText}>Done</Text>
</TouchableHighlight>
<TouchableHighlight
style={styles.buttonClear}
underlayColor='#002C7F'
onPress={this.onButtonClearPress.bind(this)}>
<Text style={styles.buttonText}>Clear</Text>
</TouchableHighlight>
</View>
);
}
};
AppRegistry.registerComponent('BasicStorageExample', () => BasicStorageExample);
The problem is that you are getting the old value of PROGRESS (before you increment by 0.2) and setting this value to the item progressbar.
So, every time you reload, React runs the componentDidMount function, and since you set progressbar to the old value of PROGRESS, it will always show it one increment behind what you see in the view.
Try changing your onButtonPress() to this:
onButtonPress() {
PROGRESS += 0.2;
AsyncStorage.setItem('progressbar', JSON.stringify(PROGRESS))
... // continues normally