Auto scale image height with React Native
React NativeReact Native Problem Overview
In my React Native app, I am fetching images from an API with unknown dimensions. How do I auto scale the height if I know my desired width?
Example:
I set the width to Dimensions.get('window').width
. How do set the height and keep the same ratio?
export default class MyComponent extends Component {
constructor(props) {
super(props)
this.state = {
imgUrl: 'http://someimg.com/coolstuff.jpg'
}
}
componentDidMount() {
// sets the image url to state
this.props.getImageFromAPi()
}
render() {
return (
<View>
<Image
source={uri: this.state.imgUrl}
style={styles.myImg}
/>
<Text>Some description</Text>
</View>
)
}
}
const styles = StyleSheet.create(
myImg: {
width: Dimensions.get('window').width,
height: >>>???what goes here???<<<
}
)
React Native Solutions
Solution 1 - React Native
Try this:
import React, { Component, PropTypes } from "react";
import { Image } from "react-native";
export default class ScaledImage extends Component {
constructor(props) {
super(props);
this.state = { source: { uri: this.props.uri } };
}
componentWillMount() {
Image.getSize(this.props.uri, (width, height) => {
if (this.props.width && !this.props.height) {
this.setState({
width: this.props.width,
height: height * (this.props.width / width)
});
} else if (!this.props.width && this.props.height) {
this.setState({
width: width * (this.props.height / height),
height: this.props.height
});
} else {
this.setState({ width: width, height: height });
}
});
}
render() {
return (
<Image
source={this.state.source}
style={{ height: this.state.height, width: this.state.width }}
/>
);
}
}
ScaledImage.propTypes = {
uri: PropTypes.string.isRequired,
width: PropTypes.number,
height: PropTypes.number
};
I'm passing the URL as a prop called uri
. You can specify your width
prop as Dimensions.get('window').width
and that should cover it.
Note that this will also work if you know what you want to set the height to and you need to resize the width to maintain the ratio. In that case, you would specify the height
prop instead of the width
one.
Solution 2 - React Native
There is a property resizeMode set it to 'contain'
Example:
<Image
source={require('./local_path_to/your_image.png')}
style={{ width: 30 }}
resizeMode="contain"
/>
Source: https://facebook.github.io/react-native/docs/image#resizemode
Edit: The above solution is working fine for me, the resizeMode property is not deprecated and I couldn't find any indications that they are planning to do so. If for some reason the the above solution doesn't work for you, you can calculate the height yourself. Here is an axample:
const Demo = () => {
const scaleHeight = ({ source, desiredWidth }) => {
const { width, height } = Image.resolveAssetSource(source)
return desiredWidth / width * height
}
const imageSource = './local_image.png'
const imageWidth = 150
const imageHeigh = scaleHeight({
source: require(imageSource),
desiredWidth: imageWidth
})
return (
<View style={{
display: 'flex',
flex: 1,
alignItems: 'center',
justifyContent: 'center'
}}>
<Image
source={require(imageSource)}
style={{
borderWidth: 1,
width: imageWidth,
height: imageHeigh
}}
/>
</View>
)
}
The above solution works only for local images. Here is how to do the same for remote images:
const RemoteImage = ({uri, desiredWidth}) => {
const [desiredHeight, setDesiredHeight] = React.useState(0)
Image.getSize(uri, (width, height) => {
setDesiredHeight(desiredWidth / width * height)
})
return (
<Image
source={{uri}}
style={{
borderWidth: 1,
width: desiredWidth,
height: desiredHeight
}}
/>
)
}
const Demo = () => {
return (
<View style={{
flex: 1,
alignItems: 'center',
justifyContent: 'center'
}}>
<RemoteImage
uri="https://via.placeholder.com/350x150"
desiredWidth={200}
/>
</View>
)
}
Solution 3 - React Native
Have a look at this library react-native-scalable-image. It does exactly what you are asking for.
import React from 'react';
import { Dimensions } from 'react-native';
import Image from 'react-native-scalable-image';
const image = (
<Image
width={Dimensions.get('window').width} // height will be calculated automatically
source={{uri: '<image uri>'}}
/>
);
Solution 4 - React Native
TypeScript version of @TheJizel answer with optional style
property and failure
callback in Image.getSize
:
import * as React from 'react'
import {Image} from 'react-native'
interface Props {
uri: string
width?: number
height?: number
style?
}
interface State {
source: {}
width: number
height: number
}
export default class ScaledImage extends React.Component<Props, State> {
constructor(props) {
super(props)
this.state = {
source: {uri: this.props.uri},
width: 0,
height: 0,
}
}
componentWillMount() {
Image.getSize(this.props.uri, (width, height) => {
if (this.props.width && !this.props.height) {
this.setState({width: this.props.width, height: height * (this.props.width / width)})
} else if (!this.props.width && this.props.height) {
this.setState({width: width * (this.props.height / height), height: this.props.height})
} else {
this.setState({width: width, height: height})
}
}, (error) => {
console.log("ScaledImage:componentWillMount:Image.getSize failed with error: ", error)
})
}
render() {
return <Image source={this.state.source} style={[this.props.style, {height: this.state.height, width: this.state.width}]}/>
}
}
Example usage:
<ScaledImage style={styles.scaledImage} uri={this.props.article.coverImageUrl} width={Dimensions.get('window').width}/>
Solution 5 - React Native
Hooks version of @TheJizel answer. I knew the width but wanted the height of the image, so the below worked for me :
const ScaledImage = props => {
const [width, setWidth] = useState()
const [height, setHeight] = useState()
const [imageLoading, setImageLoading] = useState(true)
useEffect(() => {
Image.getSize(props.uri, (width1, height1) => {
if (props.width && !props.height) {
setWidth(props.width)
setHeight(height1 * (props.width / width1))
} else if (!props.width && props.height) {
setWidth(width1 * (props.height / height1))
setHeight(props.height)
} else {
setWidth(width1)
setHeight(height1)
}
setImageLoading(false)
}, (error) => {
console.log("ScaledImage,Image.getSize failed with error: ", error)
})
}, [])
return (
height ?
<View style={{ height: height, width: width, borderRadius: 5, backgroundColor: "lightgray" }}>
<Image
source={{ uri: props.uri }}
style={{ height: height, width: width, borderRadius: 5, }}
/>
</View>
: imageLoading ?
<ActivityIndicator size="large" />
: null
);
}
Usage :
<ScaledImage width={Dimensions.get('window').width * 0.8} uri={imageurl} />
Solution 6 - React Native
I created a hook that calculates an image's aspect ratio:
function useImageAspectRatio(imageUrl) {
const [aspectRatio, setAspectRatio] = useState(1);
useEffect(() => {
if (!imageUrl) {
return;
}
let isValid = true;
Image.getSize(imageUrl, (width, height) => {
if (isValid) {
setAspectRatio(width / height);
}
});
return () => {
isValid = false;
};
}, [imageUrl]);
return aspectRatio;
}
With that you can set only one value of width or height, and calculate the other automatically:
function App() {
const aspectRatio = useImageAspectRatio(imageUrl);
return (
<Image
src={{ uri: imageUrl }}
style={{ width: 200, aspectRatio }}
/>
)
}
Solution 7 - React Native
First try this and see if it works for you: https://github.com/facebook/react-native/commit/5850165795c54b8d5de7bef9f69f6fe6b1b4763d
If it doesn't, then you can implement your own image component. But instead of taking width as prop, you override onLayout
method which gives you desired width so that you can calculate the height. This works better if you don't know the width and want RN to do the layout for you. The drawback is onLayout
is called after one pass of layout and rendering. So you might notice your components moving around a bit.
Solution 8 - React Native
Based on the answers above, I made, with TypeScript, a functional component that downloads the image only once (because the second time it will be cached: https://reactnative.dev/docs/image#getsize), if only one value is passed; and that calculates both height and width, depending on the property that was passed
import { useFocusEffect } from '@react-navigation/native';
import React from 'react';
import { ImageProps, ImageURISource } from 'react-native';
import { useIsMounted } from '../../hooks/is-mounted';
import { DrImageStyl } from './styled';
import { getImageSizes } from '../../utils/util';
interface DrSource extends ImageURISource {
uri: string;
}
interface DrImageProps extends ImageProps {
source: DrSource;
width?: number;
height?: number;
}
const DrImage: React.FC<DrImageProps> = ({
width: widthProp,
height: heightProp,
source,
...rest
}: DrImageProps) => {
const isMountedRef = useIsMounted();
const [sizes, setSizes] = React.useState({
width: widthProp,
height: heightProp,
});
useFocusEffect(
React.useCallback(() => {
const getImageSizesState = async () => {
try {
const { width, height } = await getImageSizes({
uri: source.uri,
width: widthProp,
height: heightProp,
});
if (isMountedRef.current) {
setSizes({ width, height });
}
} catch (error) {
console.log('Erro em dr-image getImageSizesState:', error);
}
};
getImageSizesState();
}, [widthProp, heightProp, source.uri])
);
return (
<>
{!!sizes.height && !!sizes.width && (
<DrImageStyl sizes={sizes} source={source} {...rest} />
)}
</>
);
};
export default DrImage;
I used a hook to determine if, after the asynchronous function, the component is still mounted (useIsMounted):
import React from 'react';
export const useIsMounted = (): React.MutableRefObject<boolean> => {
const isMountedRef = React.useRef(false);
React.useEffect(() => {
isMountedRef.current = true;
return () => {
isMountedRef.current = false;
};
}, []);
return isMountedRef;
};
I used the styled-components module to make the component's css (DrImageStyl ):
import React from 'react';
import styled, { css } from 'styled-components/native';
interface Sizes {
width?: number;
height?: number;
}
interface DrImageStylProps {
sizes: Sizes;
}
export const DrImageStyl = styled.Image<DrImageStylProps>`
${({ sizes }) => {
const { width, height } = sizes;
return css`
${width ? `width: ${width}px;` : ''}
${height ? `height: ${height}px;` : ''}
`;
}}
`;
I separated the code that calculates the other image size (getImageSizes):
import { Image } from 'react-native';
interface GetImageSizesParams {
uri: string;
height?: number;
width?: number;
}
export function getImageSizes({
height: heightParam,
width: widthParam,
uri,
}: GetImageSizesParams): Promise<{
width: number;
height: number;
}> {
return new Promise((resolve, reject) => {
function onSuccess(width: number, height: number) {
let widthResolve: number | undefined;
let heightResolve: number | undefined;
if (widthParam && !heightParam) {
widthResolve = widthParam;
heightResolve = height * (widthParam / width);
} else if (!widthParam && heightParam) {
widthResolve = width * (heightParam / height);
heightResolve = heightParam;
} else {
widthResolve = widthParam;
heightResolve = heightParam;
}
resolve({
width: widthResolve as number,
height: heightResolve as number,
});
}
function onError(error: any) {
reject(error);
}
try {
Image.getSize(uri, onSuccess, onError);
} catch (error) {
console.log('error', error);
}
});
}
Solution 9 - React Native
Here's a gist for a pretty simple solution that leverages @Haitao Li's proposal to use aspectRatio:
https://gist.github.com/tpraxl/02dc4bfcfa301340d26a0bf2140cd8b9
No magic and no calculations necessary. Pure "CSS" if you know the original image's dimensions.
Solution 10 - React Native
The proposed solution works, but you have to download image twice, once to determine the size and another to actually show the image, this is a different approach, image is loaded squared initially and resized.
import React, { Component, } from "react";
import { Image } from "react-native";
import PropTypes from 'prop-types'
export default class ScaledImage extends Component {
state = {}
componentWillMount() {
const { uri, width, height } = this.props;
this.setState({ source: { uri }, width: width || height, height: height || width });
}
render() {
return (
<Image
source={this.state.source}
onLoad={(value) => {
const { height, width } = value.nativeEvent.source;
if (this.props.width && !this.props.height) {
this.setState({
width: this.props.width,
height: height * (this.props.width / width)
});
} else if (!this.props.width && this.props.height) {
this.setState({
width: width * (this.props.height / height),
height: this.props.height
});
} else {
this.setState({ width: width, height: height });
}
}}
style={{ height: this.state.height, width: this.state.width }}
/>
);
}
}
ScaledImage.propTypes = {
uri: PropTypes.string.isRequired,
width: PropTypes.number,
height: PropTypes.number
};
Solution 11 - React Native
This one worked for me in expo
<Image style={{flex:1,width:null,height:null }} resizeMode={'contain'} source={{uri: 'http://134.209.40.60/meApunto/1567655610795_1944474896.png'}}></Image>
https://forums.expo.io/t/how-to-fit-a-big-image-into-a-fixed-container-without-resizemode-help/27639
Solution 12 - React Native
Based on @TheJizel's idea, I cooked up something using the aspectRatio style property. The following class works when the width is set, but height is omitted. This also works with percentages as width.
import React from "react";
import { Image } from "react-native";
export default class ScaledImage extends React.Component {
state = {
aspectRatio: 0
}
setAspectRatio(ratio) {
this.setState({
aspectRatio: ratio
});
}
componentWillMount() {
if (Array.isArray(this.props.source)) {
console.warn("ScaledImage received an array as source instead of local file resource or ImageURISource.")
} else if(typeof this.props.source === "number") {
// Resolve local file resource
const resolved = Image.resolveAssetSource(this.props.source);
// We assume 100% width, so we set the aspect ratio we want for it's height
this.setAspectRatio(resolved.width / resolved.height);
} else if (this.props.source.uri) {
// Resolve remote resource
Image.getSize(this.props.source.uri, (width, height) => {
this.setAspectRatio( width / height);
}, (err) => {
console.error(err);
});
} else {
console.warn("ScaledImage did not receive a valid source uri.");
}
}
render() {
if(!this.state.aspectRatio) return null;
const props = {
...this.props,
style: [this.props.style, {
aspectRatio: this.state.aspectRatio
}]
};
return (
<Image {...props} />
)
}
}
Usage:
<ScaledImage source={{ uri: "<URI HERE>" }} style={{ width: "100%" }} />
Solution 13 - React Native
You have 3 numbers :
- width of Image
- height of Image
- width of Screen
and you should put "width of Screen" in width style and also calculate height for setup in style ??!!
componentWillMount() {
Image.getSize(this.props.product.image, (width, height) => {
const screenWidth = Math.round(Dimensions.get('window').width);
this.setState({screenWidth:screenWidth});
Calculatedheight = screenWidth * height / width ;
this.setState({Calculatedheight : Calculatedheight });
});
}
and
<Image
source={{uri: product.image,cache: 'only-if-cached'}}
style={{ height: this.state.screenHeight , width: this.state.Calculatedheight }}
/>
Solution 14 - React Native
Here's some code I'm using in production. The backend user could make a logo image of any size and aspect ratio, but I needed the logo to fit an exact height with a max width. My self-scaling component is what resulted:
import React, { useState, useLayoutEffect, SFC } from "react";
import { Image } from "react-native";
import { Spinner } from "native-base";
interface INetworkImage {
targetHeight: number,
uri: string,
maxWidth: number
}
const NetworkImage: SFC<INetworkImage> = ({ uri, targetHeight, maxWidth }) => {
useLayoutEffect(() => setNaturalDimensions(uri), []);
const [imageWidth, setWidth] = useState(0);
const [imageHeight, setHeight] = useState(0);
const [scaleFactor, setScale] = useState(1);
function setNaturalDimensions(uri: string) {
Image.getSize(uri, (width: number, height: number) => {
if (width > maxWidth) {
// too wide case
setScale(maxWidth / width);
} else {
// scale to height case
setScale(targetHeight / height);
}
setWidth(width);
setHeight(height);
}, (error: any) => {
console.log("error", error);
});
}
function adjustView(e) {
if (e.nativeEvent.layout.width > maxWidth) {
setScale(scaleFactor * (maxWidth/e.nativeEvent.layout.width));
}
}
return (
imageHeight ?
<Image
onLayout={(e) => adjustView(e)}
source={{ uri: uri }}
style={{
width: imageWidth * scaleFactor,
height: imageHeight * scaleFactor,
resizeMode: "contain",
}}
/>:
<Spinner color='#454c7a' />
);
}
export default NetworkImage;
Then I use it by passing the uri, targetHeight, and maxwidth in as props:
export const deviceWidth = Dimensions.get("window").width;
<NetworkImage
uri={"https://purdyPic.com/image1"}
targetHeight={300}
maxWidth={deviceWidth * 0.85}
/>
Solution 15 - React Native
one solution out of many
<Image source={...} style={{ transform: [{ scale: 0.5 }] }} />
Solution 16 - React Native
So this all helped me a bunch
My particular scenario involved getting images from a server that could be either portrait or landscape, and I needed to fit them into a <View>
.
This means the "known" dimensions are of that view, which I obtained via onLayout
(simplified code to just show an example setting a "height"):
<View onLayout={(event) => setCellHeight(event.nativeEvent.layout.height)}>
Now with my known displayAreaHeight
and displayAreaWidth
values I need to size my image:
// Set image size for portrait/landscape scenarios, reducing the total image size when
// an overflow of the display area would occur.
if (image.height > image.width) { // Portrait Image
const ratio = displayAreaHeight / image.height;
imageHeight = displayAreaHeight;
imageWidth = image.width * ratio;
if (imageWidth > displayAreaWidth) {
const heightReductionRatio = displayAreaWidth / imageWidth;
imageHeight *= heightReductionRatio;
imageWidth = displayAreaWidth;
}
} else {
const ratio = displayAreaWidth / image.width;
imageHeight = image.height * ratio;
imageWidth = displayAreaWidth;
if (imageHeight > displayAreaHeight) {
const widthReductionRatio = displayAreaHeight / imageHeight;
imageWidth *= widthReductionRatio;
imageHeight = displayAreaHeight;
}
}
Hopefully this, along with all the other great responses here, helps someone out
Solution 17 - React Native
For both server and local images
https://www.npmjs.com/package/react-native-auto-dimensions-image
Solution 18 - React Native
No need to use any lib to achieve this instead use the below solution:
import React from 'react';
import { ImageProps } from 'react-native';
import FastImage from "react-native-fast-image";
const AutoHeightImage = React.memo(function AutoHeightImage ({ width,imageStyle, ...props }: ImageProps) {
const [state, setstate] = React.useState(0)
return (
<FastImage
{...props}
style={[{ width: width, height: state }, imageStyle]}
resizeMode={FastImage.resizeMode.contain}
onLoad={(evt) => {
setstate((evt.nativeEvent.height / evt.nativeEvent.width) * width)
}}
/>
)
})
export default AutoHeightImage;
How to use the above custom component:
<AutoHeightImage
width={(Dimensions.get('window').width)}
source={{ uri: 'image url' }}/>
React native fast image used from https://github.com/DylanVann/react-native-fast-image
Solution 19 - React Native
here is functional component sollution for using local files:
import React, {useState, useEffect} from 'react';
import {Image} from 'react-native';
import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource';
const ScaledImage = props => {
const [source, setSource] = useState(props.uri);
const [width, setWidth] = useState(props.width);
const [height, setHeight] = useState(props.height);
useEffect(() => {
let dimensions = resolveAssetSource(source);
if (props.width && !props.height) {
setWidth(props.width);
setHeight(dimensions.height * (props.width / dimensions.width));
} else if (!props.width && props.height) {
setWidth(dimensions.width * (props.height / dimensions.height));
setHeight(props.height);
} else {
setWidth(dimensions.width);
setHeight(dimensions.height);
}
}, []);
return (
<Image
source={source}
style={[{height: height, width: width}, props.style]}
/>
);
};
export default ScaledImage;
usage example:
<ScaledImage
width={Dimensions.get('window').width * 0.8}
uri={require('../../../images/Logo_Poziom.png')}
style={[
{
position: 'absolute',
top: 100,
zIndex: 1,
},
]}
/>