自动缩放图像高度与反应本机

在我的反应原生应用程序中,我从一个未知尺寸的 API 中获取图像。如果我知道我想要的宽度,如何自动缩放高度?

例如:

我设置宽度为 Dimensions.get('window').width。如何设置高度和保持相同的比例?

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???<<<
}
)
131940 次浏览

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.

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.

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>'}}
/>
);

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.

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

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

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
};

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%" }} />

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


/>

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

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

one solution out of many

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

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

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 👍

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

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

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

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

This worked for me

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

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>