Images load at different rates: uri vs require? - reactjs

I have two images that appear on the screen at different rates despite the same markup. The only difference I can see is that one image using a uri as a source while the other uses required as it's loaded from an assets directory.
This issue appears in the simulator and on an iPhone.
Code:
state = { opacity: new Animated.Value(0) };
componentDidMount() {
Animated.timing(this.state.opacity, {
toValue: 1,
duration: 1000
}).start();
}
render() {
return(
<View style={{ flex: 1, backgroundColor: 'white' }} >
<View style={{flex: 1}} >
<Animated.Image
style={{ flex:1, height: undefined, width: undefined, opacity: this.state.opacity }}
resizeMode="contain"
source={{uri: this.props.screenProps }}
/>
</View>
<View>
<Animated.Image
source={ require('../assets/images/footer.png') }
style={{height: 170, width: SCREEN_WIDTH, opacity: this.state.opacity}}
/>
</View>
</View>
);
}
}
I would like the calendar quote image and the footer image to load together.

Unfortunately when you mix external images with local images, you're going to have this problem where your local one will (hopefully) load first.
I would start off setting a state variable to handle the logic for showing your local image.
state = {
opacity: new Animated.Value(0),
isVisible: false
}
Create a function that will modify your recently added state variable
setVisibility = () => {
this.setState({
isVisible: true
})
}
(No real need for using a function here when setting state)
Inside of your render() method, simply add some logic. At first this image will not render.
{this.state.isVisible &&
<View>
<Animated.Image
source={require('../assets/images/footer.png')}
style={{ height: 170, width: SCREEN_WIDTH, opacity: this.state.opacity }}
/>
</View>}
Here's where the real magic happens, Image in React Native comes with an onLoadEnd event.
Invoked when load either succeeds or fails.
You can provide additional checks for success or failure, this is not included within this example. Simply add the onLoadEnd prop to your external image component and pass setVisibility as the event handler.
<Animated.Image
style={{ flex: 1, height: undefined, width: undefined, opacity: this.state.opacity }}
resizeMode="contain"
source={{ uri: this.props.screenProps }}
onLoadEnd={this.setVisibility}
/>
You can seen an example on Expo here.

Related

Conditional rendering cannot be seen due to a return before them

Please help give me a better title, I couldn't word what I am asking.
To understand my question I have to provide some context of my project. Originally I wanted to conditionally render two pages through two buttons. Button A rendering screen A and button B rendering screen B. After figuring out how to pass the state from a parent component to a child and its child etc, I changed my button to a sliding animation for better design.
This causes issues because now when a new screen is rendered, the animation does not show because it is simply re-rendered with the original starting place in the animation(I rendered the slider through each screen). I thought about providing two different sliders, each starting in the opposing opposition but that would still lose the entire slide effect.
I have now resulted to rendering the Slider so it is there all the time and is not re-rendered. However I have realized now that if I return it before my conditionals, that code is never reached. I have provided a working demo that shows my problem perfectly as well as the code below(I only provided App.js, the rest is on the demo if needed). I want to render Slider in App.js.
The working demo is here, you can see the slider does not slide, it just changes screens. I need it to slide. Also the sliding animation only works on iphone so I would use that emulator rather than the web.
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
whichComponentToShow: "Screen1"
};
}
goToMap = () => {
this.setState({ whichComponentToShow: "Screen2" });
};
goToList = () => {
this.setState({ whichComponentToShow: "Screen1" });
};
render() {
const { whichComponentToShow } = this.state;
/* This is how I thought I could render this, but obv it makes the rest of the code unreachable.
How can I render this and then have the conditional page below? Each time the new page renders,
it stops the animation from working due to rendering the new page.
return(
<Slider/>
)*/
if(this.state.whichComponentToShow === 'Screen1'){
return(
<View style={{backgroundColor: '#d1cfcf' ,flex: 1}}>
<ListHome
renderMap = {this.goToMap.bind(this)}
renderList = {this.goToList.bind(this)}
/>
</View>
);
}
else if(this.state.whichComponentToShow === 'Screen2'){
return(
<View style={{backgroundColor: '#d1cfcf' ,flex: 1}}>
<MapHome
renderMap = {this.goToMap.bind(this)}
renderList = {this.goToList.bind(this)}
/>
</View>
);
}
Slider.js (wont show up on the snack apparently
const Slider = (props) => {
const [active, setActive] = useState(false)
let transformX = useRef(new Animated.Value(0)).current;
useEffect(() => {
if (active) {
Animated.timing(transformX, {
toValue: 1,
duration: 300,
useNativeDriver: true
}).start()
} else {
Animated.timing(transformX, {
toValue: 0,
duration: 300,
useNativeDriver: true
}).start()
}
}, [active]);
const rotationX = transformX.interpolate({
inputRange: [0, 1],
outputRange: [2, Dimensions.get('screen').width / 4]
})
return (
<SafeAreaView style={{
flex: 1,
alignItems: 'center'
}}>
<View style={{
flexDirection: 'row',
position: 'relative',
height: 45,
width: 240,
borderRadius: 10,
backgroundColor: 'white',
marginHorizontal: 5
}}>
<Animated.View
style={{
position: 'absolute',
height: 45 - 2*2,
top: 2,
bottom: 2,
borderRadius: 10,
width: Dimensions
.get('screen').width / 3 - 3.5 ,
transform: [
{
translateX: rotationX
}
],
backgroundColor: '#d1cfcf',
}}
>
</Animated.View>
<TouchableOpacity style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}} onPress={() => {setActive(false); props.renderList() }}>
<Text>
List
</Text>
</TouchableOpacity>
<TouchableOpacity style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}} onPress={() => {setActive(true); props.renderMap() }}>
<Text>
Map
</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
}
export default Slider
I tried your expo snack and saw no code related to animation, so I'm assuming the code in the snack isn't your current code, and that you really do already have a working, animated, <Slider ... />.
In your situation, what you could do to keep your Slider rendered and not unmounted, is to use variables in the render() method.
Basically, you can assign the <Slider .../> JSX to a variable, and you can use that variable in another JSX part later.
Assigning a key to the specific JSX also helps guide React that this is the same component between render calls, so it also prevents unintentional rerenders of that component.
Here's an edit with comments from what you wrote in your post. I hope this makes sense.
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
whichComponentToShow: "Screen1"
};
}
goToMap = () => {
this.setState({ whichComponentToShow: "Screen2" });
};
goToList = () => {
this.setState({ whichComponentToShow: "Screen1" });
};
render() {
const { whichComponentToShow } = this.state;
/*
Keep the slider JSX in a variable to be used.
Setting a specific key also helps prevent it from being accidentally re-rendered in some conditions.
)*/
const sliderRender = <Slider key='slider' />;
if (this.state.whichComponentToShow === 'Screen1') {
return (
<View style={{ backgroundColor: '#d1cfcf', flex: 1 }}>
<ListHome
renderMap={this.goToMap.bind(this)}
renderList={this.goToList.bind(this)}
/>
{/* Put the rendered slider into the render tree */}
{sliderRender}
</View>
);
}
else if (this.state.whichComponentToShow === 'Screen2') {
return (
<View style={{ backgroundColor: '#d1cfcf', flex: 1 }}>
<MapHome
renderMap={this.goToMap.bind(this)}
renderList={this.goToList.bind(this)}
/>
{/* Put the rendered slider into the render tree */}
{sliderRender}
</View>
);
}
}
}
Edit : Expo Snack demonstrating it working

react native - Image not showing on custom callout in mapview?

I have a map screen with markers, I try to add image to the callout and I use same method of <image source = .. /> as I did in other place that works, but on the map it wont show me the picture.
{
this.state.markers.map(marker => (
<MapView.Marker
key={marker.id}
coordinate={{longitude: marker.longitude, latitude: marker.latitude}}>
<MapView.Callout>
<View>
<View>
{marker.imageUri && <Image source = {{uri: marker.imageUri}}
style = {{ width: '90%', height: 100, justifyContent: 'center', flex: 1, alignContent: 'center', resizeMode: 'stretch'}}
/> }
</View>
<Text>Lat: {marker.latitude}, Lon: {marker.longitude}</Text>
<Text>{marker.email}</Text>
</View>
</MapView.Callout>
</MapView.Marker>
))
}
it gives me a blank view instead of the image.
Have I done any mistake?
Use Image inside Text component,It is strange but it works :)
<Text> <Image style={{ height: 100, width:100 }} source={{ ... }} resizeMode="cover" /> </Text>
https://docs.expo.io/versions/latest/sdk/webview/
import { WebView } from 'react-native-webview';
If const isAndroid = Platform.OS === 'android'; is true render a WebView... if not render a Image.
I fixed it by using a webView for android: const Img = isAndroid ? WebView: Image; return <Img source={{uri: someUri}} />

onLoadEnd is not fired in react-native Image

hi am trying to load an remote image. onLoadStart is hitting but not the onLoadEnd
`
<View style={{ paddingTop: 60, paddingBottom: 10 }}>
{this.state.loading ? (
<DotIndicator size={25} color={"white"} />
) : (
<Image
resizeMode={this.resizeMode}
style={[styles.imageStyle, this.tintStyle]}
onLoadStart={e => {
this.setState({ loading: true });
}}
onLoadEnd={e => this.setState({ loading: false })}
// defaultSource={NoProfile}
// loadingIndicatorSource={require("#images/profile_placeholder.png")}
source={this.userImageUri}
onError={error => {
this.tintStyle = { tintColor: "lightgray" };
this.resizeMode = "contain";
this.userImageUri = NoProfile;
}}
/>
)}
</View>
`
EDIT 1
onLoadStart is hit. onLoad is also never being called
does any one have a clue. am new to react.
Any help is appreciated.
thanks is advance
SOLUTION
Since Vignesh and hong mentioned the image is never there so its on loadEnd will never be called. So instead of loading only image or loader I loaded the loader on top of image. Posting this here as it may be useful for someone at sometime. Once again thanks to Vignesh and hong
<View
style={{
padding: 10,
width: WIDTH - 50,
height: WIDTH - 25,
alignSelf: "center"
}}
>
{this.state.loading ? (
<MaterialIndicator
size={50}
color={"red"}
style={{
marginTop: WIDTH / 2,
alignSelf: "center"
}}
/>
) : null}
<Image
resizeMode={this.resizeMode}
style={[styles.imageStyle, this.tintStyle]}
onLoadStart={e => {
this.setState({ loading: true });
}}
onLoad={e => {
this.setState({ loading: false });
}}
onLoadEnd={e => this.setState({ loading: false })}
// defaultSource={NoProfile}
// loadingIndicatorSource={require("#images/profile_placeholder.png")}
source={this.userImageUri}
onError={error => {
this.tintStyle = { tintColor: "lightgray" };
this.resizeMode = "contain";
this.userImageUri = NoProfile;
}}
/>
</View>
Let's say that the value of this.state.loading was false before the first render.
When the first render happens, this.state.loading ? returns the Image component, onLoadStart is triggered and this.state.loading is set to true.
When the second render happens, this.state.loading is found to be true and this.state.loading ? returns the DotIndicator component. All the hard work done by the Image component during the previous the render is lost. In fact, Image component was never present in that context.
Hence, onLoadingEnd will never be triggered, because Image component never appeared in the second render.
And the DotIndicator will forever go round and round and round... Waiting for it's lost love..
If loading is true from the beginning, nothing will be called in the image.
If loading value is false at first, the image does not see loadStart running, only load function and loadEnd function will be called.
This is because the loadStart function runs when the start value is false and it is already rendered. And if the start value is true, nothing is done because it does not draw an image.
This is a very simple example that you can experiment with:
import React, { Component } from 'react';
import { View, Image } from 'react-native';
export default class App extends Component {
state={
loading: false,
}
render() {
return (
<View>
{this.state.loading ? (
<Image
style={{width: 100, height: 100}}
source={{uri: 'https://facebook.github.io/react-native/img/tiny_logo.png'}}
/>
) : (
<Image
style={{width: 50, height: 51}}
onLoadStart={e => this.setState({ loading: true })}
onLoad={e => alert("onLoad")}
onLoadEnd={e => alert("onLoadEnd")}
source={require('#expo/snack-static/react-native-logo.png')}
/>)}
</View>
);
}
}
I ended up copying the same image but set its style to width: 1, height: 1. this will always be displayed but is not visible. What this allows is to continue to set the state despite the many rerenders. I then in a if statement have the actual image or a text component that will display image not found. By default NoImageFound is set to true. Here is my code
<Image
source={{ uri: image }}
style={{ width: 1, height: 1 }}
onLoadStart={() => setNoImageFound(true)}
onLoad={() => setNoImageFound(false)}
/>
{noImageFound ? (
<View style={{ justifyContent: "center", alignItems: "center" }}>
<Text style={{ textAlign: "center" }}>No image found</Text>
</View>
) : (
<View style={styles.icon}>
<Image source={{ uri: image }} style={styles.image} />
</View>
)}

How to Handle overflowing elements in React-native?

How to use flex in react-native to make any element inside <View></View> to arrange themselves when their length got bigger than their container's ?
Below is my code:
const FilmCasts = ({ artists }) => (
<View style={{ flex: 1, flexDirection: 'row', }}>
{artists.map(artist => (
<Image
key={cast.name}
source={{ uri: artist.img }}
style={{ width: 80, height: 100 }}
/>
))}
</View>
);
This is what I want. I want to make the 5th and others to go below the first row automatically after they hit the screen width limit, like what HTML usually does
But instead, this is what I get when in React Native, the <Image> elements continue to be rendered outside their parent's width
You can use the prop flexWrap:'wrap' for wrapping the elements. flexWrap controls whether children can wrap around after they hit the end of a flex container. More here
const FilmCasts = ({ artists }) => (
<View style={{ flex: 1, flexDirection: 'row',flexWrap:'wrap' }}>
{artists.map(artist => (
<Image
key={cast.name}
source={{ uri: artist.img }}
style={{ width: 80, height: 100 }}
/>
))}
</View>
);
You can simply defined image with dynamically using the Dimension property of react native .
like when you are using
<Image
resizeMode="cover" or try "contain"
key={cast.name}
source={{ uri: artist.img }}
style={{ width: width * 0.5 , height: 100 }} // this will be done currently defining the values .
/>
Here is one blog regarding image handling in react native Click to see the blog
Hope my answer will give an idea to slove your problem

Detect tap on the outside of the View in react native

How to detect tap on the outside of the View(View is a small one width and height are 200). For example, I have a custom View(which is like a modal) and it's visibility is controlled by state. But when clicking outside of it nothing is changed because there is no setState done for that, I need to catch users tap everywhere except inside the modal. How is that possible in React Native?
use a TouchableOpacity around your modal and check it's onPress. Look at this example.
const { opacity, open, scale, children,offset } = this.state;
let containerStyles = [ styles.absolute, styles.container, this.props.containerStyle ];
let backStyle= { flex: 1, opacity, backgroundColor: this.props.overlayBackground };
<View
pointerEvents={open ? 'auto' : 'none'}
style={containerStyles}>
<TouchableOpacity
style={styles.absolute}
disabled={!this.props.closeOnTouchOutside}
onPress={this.close.bind(this)}
activeOpacity={0.75}>
<Animated.View style={backStyle}/>
</TouchableOpacity>
<Animated.View>
{children}
</Animated.View>
</View>
const styles = StyleSheet.create({
absolute: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'transparent'
},
container: {
justifyContent: 'center',
elevation: 10,
}
});
<View
onStartShouldSetResponder={evt => {
evt.persist();
if (this.childrenIds && this.childrenIds.length) {
if (this.childrenIds.includes(evt.target)) {
return;
}
console.log('Tapped outside');
}
}}
>
// popover view - we want the user to be able to tap inside here
<View ref={component => {
this.childrenIds = component._children[0]._children.map(el => el._nativeTag)
}}>
<View>
<Text>Option 1</Text>
<Text>Option 2</Text>
</View>
</View>
// other view - we want the popover to close when this view is tapped
<View>
<Text>
Tapping in this view will trigger the console log, but tapping inside the
view above will not.
</Text>
</View>
</View>
https://www.jaygould.co.uk/2019-05-09-detecting-tap-outside-element-react-native/
I found these solution here, hope it helps
Wrap your view in TouchableOpacity/TouchableHighlight and add onPress Handler so that you can detect the touch outside your view.
Something like :
<TouchableOpacity onPress={() => {console.log('Touch outside view is detected')} }>
<View> Your View Goes Here </View>
</TouchableOpacity>

Resources