功能概述
AdaptiveImage 是一个自适应宽高的图片组件,当 style prop 中只提供一个维度(宽度或高度)时,会自动计算另一个维度以保持图片的原始宽高比。支持本地图片和网络图片。
使用示例
typescript
import AdaptiveImage from './AdaptiveImage';
// 本地图片 - 只设置高度,宽度自动计算
<AdaptiveImage
source={require('./local.png')}
style={{height: 200}}
/>
// 网络图片 - 只设置宽度,高度自动计算
<AdaptiveImage
source={{uri: 'https://example.com/image.jpg'}}
style={{width: 300}}
/>
实现原理
- 组件通过
Image.getSize
(网络图片)或Image.resolveAssetSource
(本地图片)获取原始图片尺寸 - 根据用户设置的width或height,按比例计算另一个维度
- 使用
StyleSheet.flatten
合并用户样式和计算样式 - 通过
onLayout
事件监听容器尺寸变化并重新计算
源码
typescript
import React, {Component} from 'react';
import {
DimensionValue,
Image,
ImageSourcePropType,
ImageURISource,
StyleSheet,
} from 'react-native';
import {ImageProps} from 'react-native/Libraries/Image/Image';
interface AdaptiveImageProps extends ImageProps {}
interface AdaptiveImageState {
/** 图片根据 props 计算后的宽度 (如果需要计算) */
calculatedWidth: number | null;
/** 图片根据 props 计算后的高度 (如果需要计算) */
calculatedHeight: number | null;
}
/**
* 一个 Class Component 图片组件,
* 当 style prop 中只提供了一个维度(宽度或高度)时,
* 它会自动计算另一个维度以保持图片的原始宽高比。
*
* 注意:此组件支持本地图片和网络图片。
*/
class AdaptiveImage extends Component<AdaptiveImageProps, AdaptiveImageState> {
constructor(props: AdaptiveImageProps) {
super(props);
this.state = {
calculatedWidth: null,
calculatedHeight: null,
};
}
/**
* 根据传入的 props 计算图片尺寸
* @param props - 组件的 props
* @param layoutWidth - 布局宽度
* @param layoutHeight - 布局高度
*/
calculateDimensions = (
props: AdaptiveImageProps,
layoutWidth: number,
layoutHeight: number,
) => {
const {source, style} = props;
// 扁平化样式并提取数字类型的宽度和高度
const flatStyle = StyleSheet.flatten(style || {});
const styleWidth = flatStyle.width;
const styleHeight = flatStyle.height;
// 检查是否为网络图片
const isNetworkImage = typeof source === 'object' && 'uri' in source;
if (isNetworkImage) {
// 网络图片,使用 Image.getSize 获取宽高
const uri = (source as ImageURISource).uri;
Image.getSize(
uri ?? '',
(width, height) => {
// 成功获取到宽高
this.updateDimensions(
width,
height,
layoutWidth,
layoutHeight,
styleWidth,
styleHeight,
);
},
() => {
// 加载失败
},
);
} else {
// 本地图片,使用 resolveAssetSource 获取宽高
const imageInfo = Image.resolveAssetSource(source as ImageSourcePropType);
if (!imageInfo || imageInfo.width <= 0 || imageInfo.height <= 0) {
return;
}
this.updateDimensions(
imageInfo.width,
imageInfo.height,
layoutWidth,
layoutHeight,
styleWidth,
styleHeight,
);
}
};
/**
* 更新图片的宽高状态
* @param imgWidth - 图片原始宽度
* @param imgHeight - 图片原始高度
* @param styleWidth - 用户传入的宽度
* @param styleHeight - 用户传入的高度
*/
updateDimensions = (
imgWidth: number,
imgHeight: number,
layoutWidth: number,
layoutHeight: number,
styleWidth?: DimensionValue,
styleHeight?: DimensionValue,
) => {
let newCalculatedWidth: number | null = null;
let newCalculatedHeight: number | null = null;
if (styleHeight && !styleWidth) {
// 高度确定,宽度不确定 -> 根据高度和图片比例计算宽度
newCalculatedWidth = (layoutHeight / imgHeight) * imgWidth;
} else if (styleWidth && !styleHeight) {
// 宽度确定,高度不确定 -> 根据宽度和图片比例计算高度
newCalculatedHeight = (layoutWidth / imgWidth) * imgHeight;
}
// 仅在计算值与当前 state 中的值不同时,才更新 state,防止无限循环
if (
newCalculatedWidth !== this.state.calculatedWidth ||
newCalculatedHeight !== this.state.calculatedHeight
) {
this.setState({
calculatedWidth: newCalculatedWidth,
calculatedHeight: newCalculatedHeight,
});
}
};
render() {
const {style, onLayout, ...restProps} = this.props;
const {calculatedWidth, calculatedHeight} = this.state;
// 合并样式:基础样式 + 用户传入样式 + 计算出的覆盖样式
const finalStyle = StyleSheet.flatten([
style, // 用户提供的样式
calculatedWidth !== null && {width: calculatedWidth}, // 应用计算出的宽度 (如果存在)
calculatedHeight !== null && {height: calculatedHeight}, // 应用计算出的高度 (如果存在)
]);
return (
<Image
style={finalStyle}
onLayout={event => {
const layoutWidth = event.nativeEvent.layout.width;
const layoutHeight = event.nativeEvent.layout.height;
this.calculateDimensions(this.props, layoutWidth, layoutHeight);
onLayout?.(event);
}}
{...restProps}
/>
);
}
}
// 导出重命名后的组件
export default AdaptiveImage;