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???<<<
  }
)

Solution 1

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

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

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

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 5

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 6

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

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

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

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

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

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

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

You have 3 numbers :

  1. width of Image
  2. height of Image
  3. 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

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

one solution out of many

<Image source={...} style={{ transform: [{ scale: 0.5 }] }} />

Solution 16

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

Solution 18

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

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,
    },
  ]}
/>

Solution 20

This worked for me

 <Image source={{ uri }} style={{width:"100%", height:'100%'}} 
 resizeMode='contain'/>

Solution 21

if you want to scale the image automatically also wish to apply the default image when the image is in the rendering process use this

import ScalableImage from 'react-native-scalable-image';

here combined is variable in witch API response is saved and large_image is variable which contain image address and ImageURL variable contain image base path

 for (let i = 0; i < combined.length; i++) {
            Image.getSize(
              ImageURL + combined[i].large_image,
              (width, height) => {
                combined[i].imageSize = {
                  'ImageHeight': (height).toString(),
                  'ImageWidth': (width).toString(),
                };
              },
            );
          }

this is how we show image with default image with different height and width without cropping

 <ImageBackground
                          source={require('../Assets/Icons/thumbnail.png')}
                          style={{
                            width: Dimensions.get('window').width,
                            height: undefined,
                            aspectRatio:item?.imageSize? item?.imageSize?.ImageWidth / item?.imageSize?.ImageHeight : 1
                          }}>
                          <ScalableImage
                            width={Dimensions.get('window').width}
                            source={{uri: ImageURL + item.large_image}}
                          />
                        </ImageBackground>