I am trying to make a parallax background like the react-parallax package.
I couldn't use react-parallax package it gives me 504 error (problem with vite).
I tried to use #react-spring/parallax but it didn't do the job.
Finally I wrote my own code but I still confused wether my code has a bad performace.
I've made a scroll listener with a callback which sets scroll state to scrollY value
then using this value with background position y
useEffect(() => {
window.addEventListener("scroll", () => setscroll(window.scrollY));
return () => {
window.removeEventListener("scroll", () =>
setscroll(window.scrollY)
);
};
}, []);
<Swiper>
<SwiperSlide
style={{
backgroundImage: `url(${Img[0]})`,
backgroundSize: "cover",
backgroundPositionX: "center",
backgroundPositionY: `${(scroll - 60) * 0.3}px`,
width: "100%",
backgroundRepeat: "no-repeat",
overflow: "hidden",
position: "relative",
}}></SwiperSlide>
</Swiper>
The performace issue I touched is this component renders every time I scroll.
What should I do?
I am using the Vector Icons provided by Expo https://github.com/expo/vector-icons
All the icons accept a size prop which is an integer, my use case is to see if there to adjust the icon size automatically based on the parent container without having to worry about the size on each and every device (because size seems to translate the same on every device despite the pixel ratio, dimensions etc)
<View style={{flex: 1}}>
<Icon name="app-logo" size={30} color="white" />
</View>
I tried setting style prop to the below options but no luck. I would expect that this will not work because after all they are just fonts and they need not support the width and height
<Icon name="app-logo" style={{flex: 1}} color="white" />
<Icon name="app-logo" style={{width: '100%', height: '100%' color="white" />
Is there any cleaner or suggested way to do this? Using dimensions and ratios of the devices is my last resort.
Here's an example of the closest I've been able to get:
import React from "react"
import { PixelRatio, StyleSheet, Text, View } from "react-native"
import { FontAwesome5 } from "#expo/vector-icons"
const getRandomNumber = (min: number, max: number) =>
Math.random() * (max - min) + min
const App = () => {
const [containerSize, setContainerSize] = React.useState<number>()
const randomDimension = React.useMemo(() => getRandomNumber(100, 400), [])
return (
<View
style={{
flex: 1,
backgroundColor: "green",
justifyContent: "center",
alignItems: "center",
}}
>
<View
style={{
width: randomDimension,
aspectRatio: 1,
backgroundColor: "red",
}}
onLayout={layoutEvent =>
setContainerSize(layoutEvent.nativeEvent.layout.width)
}
>
<FontAwesome5
name="app-store-ios"
size={containerSize || 0}
style={{
textAlign: "center",
}}
color="blue"
/>
</View>
</View>
)
}
export default App
There's some code in here for intentionally setting the icon's parent container to a randomly sized square, and setting that on the parent, which is not what you're looking for but helped me re-create the problem.
The interesting bit which I think you want is the value passed to the onLayout prop of the icon's parent container, which gets the parent container's size and puts it in state. That state will be empty until the first render & layout, so we have the size of the vector icon defaulted to 0 until that happens. Once we have the size of the parent in state, we can set the size of the vector icon.
The vector icon doesn't actually fill the entire parent's space, but it scales pretty close pretty well. I think that's because the vector icon itself is a font under the hood (?) and we're trying to set the size of a font (in pts really (I think?)) to the size we got from the parent in pixels. There's probably a neat way to clean that up using react native's PixelRatio - I spent a bit of time trying but couldn't get it working perfectly.
The last thing I needed to do (since the vector icon doesn't actually "fill" the parent view was set the style prop of the icon to { textAlign: 'center' } which keeps it centered horizontally within the parent view (it was already centered vertically) instead of being off on the left side of the parent.
You could jank the whole thing into submission by adding a constant to the size and putting a negative top margin on the icon, but that's kind of a PITA and obviously not a great solution (it'll be a headache every time you do it and it probably won't work cross platform / on different pixel densities).
The reason I actually arrived at this post was because I was trying to keep an icon the same size as text; I was working on an app which showed a comment count (integer) to the left of a little chat bubble icon and I wanted the icon's size to scale with the system's font size if the user had adjusted that in their system settings. This is actually easier (which makes sense given my theory about vector icons being represented as fonts under the hood is correct). Here's the relevant part of my code that achieved that:
const StoryCommentsLabel = ({ story }: { story: HackerNewsItem }) => {
const navigation = useNavigation()
const numberOfComments = story.descendants || 0
const doNotActTouchable = { activeOpacity: 1 }
const fontScale = React.useMemo(() => PixelRatio.getFontScale(), [])
const defaultFontSize = 14
const iconSize = defaultFontSize * fontScale
return (
<TouchableOpacity
style={{ flexDirection: "row", justifyContent: "flex-end" }}
onPress={() => {
if (numberOfComments > 0) {
navigation.navigate("Story Comments", { story })
}
}}
hitSlop={{ top: 20, left: 20, bottom: 20, right: 20 }}
{...(numberOfComments === 0 ? doNotActTouchable : {})}
>
<Text
style={{
fontSize: 14,
color: PlatformColor("secondaryLabel"),
}}
>
{numberOfComments}
</Text>
<View style={{ width: 5 }} />
<Ionicons
name="chatbubble"
size={iconSize}
color={PlatformColor("secondaryLabel")}
/>
</TouchableOpacity>
)
}
There's more in there than you need, but the important bit is this:
const fontScale = React.useMemo(() => PixelRatio.getFontScale(), [])
const defaultFontSize = 14
const iconSize = defaultFontSize * fontScale
I'm trying to add a border to the image when its clicked, like a radio button, but only with an image.
The code works perfectly on web but when I try to click the image on the device it doesn't do anything!
I tried to do Alert.alert() and it's like it doesn't register the click.
let stylereg_na = { borderRadius: 3, height: hp('7%'), width: wp('25%')};
let stylereg_eu = { borderRadius: 3, height: hp('7%'), width: wp('25%')};
if(regNA)
{
stylereg_na = {borderWidth: 2, borderColor:'black', borderRadius: 3, height: hp('8%'), width: wp('26%')};
}
return (
<Image style={stylereg_na} source={na} onClick={() =>
{
if(regNA)
setRegNAStatus(false);
else
{
setRegNAStatus(true);
}
}
);
You need to wrap it Touchable component like this
<TouchableWithoutFeedback onPress={() => alert('Pressed!')}>
<Image style={stylereg_na} source={na} />
</TouchableWithoutFeedback>;
refer to the documentation if you wanna learn more about it, like TouchableHighlight or TouchableOpacity https://reactnative.dev/docs/touchablewithoutfeedback
I'm new to React Spring and I initially tried this
const strikeProps = useSpring({
textDecoration: "line-through",
from: { textDecoration: "none" },
});
But it's not working. I think there should be a way simulate the CSS solution for this.
The problem here is, that the original CSS solution is uses pseudo element for emulating the strike trough. We can only add react-spring properties for normal html elements. So the most compact way is to create a separate strike through component for this problem. For example:
const StrikeTroughtText = ({ children, weight = 1 }) => {
const props = useSpring({
from: { width: "0%" },
to: { width: "100%" }
});
return (
<div style={{ position: "relative", display: "inline-block" }}>
{children}
<animated.div
style={{
position: "absolute",
top: "50%",
left: 0,
width: props.width,
height: `${weight}px`,
background: "black"
}}
/>
</div>
);
};
We basically animate the width of the absolutely positioned div containing a black line over the text.
You can use it like a div component:
<StrikeTroughtText>text</StrikeTroughtText>
For bigger font size the default 1 px line weight is not enough, so I added a weight property also.
Here is my example: https://codesandbox.io/s/react-strike-trought-text-component-with-react-spring-animation-86cfd?file=/src/App.js
The goal: have a dropdown view that animates the height-expansion over time. the caveat is this: once the view is expanded, it needs to be able to dynamically handle whether or not there is additional view data present. if it is present, a couple extra text components will be rendered.
The problem: as it currently is, the animation the parent's height to a fixed height. so when the additionalContent is rendered, it surpasses the bounds of the parent, whose height is fixed. I dont want to not set the height of the parent explicitly, because then I cant animate that aspect the way I want. I want to maintain the height animation as-is, as well as dynamically size the parent to contain the children when the additionalContent is present
const ListItem = (props) => {
const [checkInModal, setCheckInModal] = useState(false);
const [animatedHeight, setAnimatedHeight] = useState(new Animated.Value(0))
const [animatedOpacity] = useState(new Animated.Value(0))
const [dynamicHeight, setDynamicHeight] = useState(0);
const [expanded, setExpanded] = useState(false);
const toggleDropdown = () => {
if (expanded == true) {
// collapse dropdown
Animated.timing(animatedHeight, {
toValue: 0,
duration: 200,
}).start()
} else {
// expand dropdown
Animated.timing(animatedHeight, {
toValue: 100,
duration: 200,
}).start()
}
setExpanded(!expanded)
}
const renderAdditionalContent = () => {
setDynamicHeight(75);
if (someVariable == true) {
return (
<View> <Text> Some Content </Text> </View>
)
}
}
const interpolatedHeight = animatedHeight.interpolate({
inputRange: [0, 100],
outputRange: [75, 225]
})
const interpolatedOpacity = animatedOpacity.interpolate({
inputRange: [0, 100],
outputRange: [0.0, 1.0]
})
return (
<Animated.View
style={[styles.container, { height: interpolatedHeight + dynamicHeight }]}
>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', }}>
<View style={styles.leftContainer}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={styles.title}>{props.title}</Text>
</View>
<Text style={styles.subtitle}>{time()}</Text>
</View>
<View style={styles.rightContainer}>
<TouchableOpacity onPress={() => toggleDropdown()} style={styles.toggleBtn}>
<Image source={require('../assets/img/chevron-down.png')} resizeMode={'contain'} style={styles.chevron} />
</TouchableOpacity>
</View>
</View>
{expanded == true ? (
<Animated.View style={[styles.bottomContainer, { opacity: interpolatedOpacity }]}>
<Components.BodyText text="Subject:" style={{ fontFamily: Fonts.OPENSANS_BOLD }} />
<Components.BodyText text={props.subject} />
{ renderAdditionalContent() }
</Animated.View>
) : null}
</Animated.View>
);
};
const styles = StyleSheet.create({
container: {
backgroundColor: '#fff',
borderRadius: 25,
width: width * 0.95,
marginBottom: 5,
marginHorizontal: 5,
paddingVertical: 15,
paddingHorizontal: 15
},
leftContainer: {
justifyContent: 'space-between',
},
rightContainer: {
flexDirection: 'row',
alignItems: 'center'
},
title: {
fontFamily: Fonts.OPENSANS_BOLD,
fontSize: 20,
color: '#454A66'
},
subtitle: {
color: '#454A66',
fontSize: 14
},
typeIcon: {
height: 25,
width: 25
},
chevron: {
height: 15,
width: 15
},
toggleBtn: {
borderWidth: 1,
borderColor: Colors.PRIMARY_DARK,
borderRadius: 7,
paddingTop: 4,
paddingBottom: 2.5,
paddingHorizontal: 4,
marginLeft: 10
},
bottomContainer: {
marginVertical: 20
},
buttonContainer: {
flexDirection: 'row',
width: 250,
justifyContent: 'space-between',
alignSelf: 'center',
marginVertical: 20
},
noShadow: {
elevation: 0,
shadowOffset: {
width: 0,
height: 0
},
shadowRadius: 0,
}
});
export default ListItem;
How can this be accomplished? So far ive tried creating a state variable dynamicHeight and setting it inside the function that renders additional content, but that hasn't worked.
Heres the snack: https://snack.expo.io/P6WKioG76
clarification edit: renderAdditionalContent function renders additional content (obviously), this content could be anywhere from one line of characters to multiple lines. regardless of the char count, the main parent container of the component needs to have all the children within its bounds. as it stands, if the additional-content-rendered has too much content, the content will spill over the border of the component's main parent container, which must be avoided. this can be done by simply not giving a height to the main component container, obviously. but the the idea is to have the animated height AND wrap the child content properly
Any suggestions?
EDITED : easy and simple way
You can also apply the height 100%, in that way we don't need to calculate the height of inner content it will auto adjust as per the content provided
const interpolatedHeight = animatedHeight.interpolate({
inputRange: [0, 100],
outputRange: ["0%", "100%"] // <---- HERE
})
To make this happen I have added <View> tag around the <Animated.View> to get height 100% correct and few changes in css, this looks more elegant and provides a perfect solution to your problem.
WORKING DEMO
So, you can use onLayout
This event is fired immediately once the layout has been calculated
First step:
// setHeight
const setHeight = (height) => {
setDynamicHeight(prev => prev + height);
}
<View onLayout={(event) => {
var { x, y, width, height } = event.nativeEvent.layout;
setHeight(height); // <--- get the height and add it to total height
}}>
<Text>Subject</Text>
<Text>Subject Content</Text>
{renderAdditionalContent()}
</View>
Second step:
useEffect(() => {
// trigger if only expanded
if (expanded) {
// trigger height animation , whenever there is change in height
Animated.timing(animatedHeight, {
toValue: dynamicHeight, // <--- animate to the given height
duration: 200,
}).start();
}
}, [dynamicHeight]); // <--- check of height change
WORKING DEMO (you can test it by adding remove text)
Here's a working snack of your code with a dynamic dropdown height: https://snack.expo.io/4mT5Xj6qF
You can change the height by changing the thisIsAValue constant.
Honestly you were very close with your code, you just had a few bits and pieces missing:
When you're animating height you don't need to use interpolate, you can let the <Animated.View> straight up calculate the height. The interpolation is needed if you want to animate, for example, a rotation, and you need to calculate the degrees to which the element must move.
You need to pass your dynamic height into the animation, as your to: value
I set up a simple useEffect hook checking for changes in the someVariable and dynamicHeight prop
This solution will let you set the height dynamically through a prop.
However if you want to calculate the height based on elements which are present in the View you may want to check #vivek-doshi 's answer
You can use React refs. This allows you to access a component directly.
const el = React.useRef(null);
Assign el to the ref of the container div of whatever content you have. Then hide/show the div using the visibility style:
<div style={{ visibility: !expanded && "hidden" }} ref={el}>
<View /* snip */>
{ renderAdditionalContent() }
/* -- snip -- */
</View>
</div>
Then in the toggleDropdown function you can access the height of the component via the .current.offsetHeight property:
Animated.timing(animatedHeight, {
toValue: el.current.offsetHeight,
duration: 200,
}).start()
Refs can be used on any html element and allow you to access the element's raw properties. Animations are probably one of the most common use cases for them.
Demo: https://snack.expo.io/heLsrZNpz
You can read more here: https://reactjs.org/docs/refs-and-the-dom.html