【React Native】自适应宽高的图片组件AdaptiveImage

功能概述

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

实现原理

  1. 组件通过Image.getSize(网络图片)或Image.resolveAssetSource(本地图片)获取原始图片尺寸
  2. 根据用户设置的width或height,按比例计算另一个维度
  3. 使用StyleSheet.flatten合并用户样式和计算样式
  4. 通过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;
相关推荐
T_Donna12 小时前
【问题解决】react native: cli.init is not a function
javascript·react native·react.js
miao_zz14 小时前
FlipperKit报错
react native
wayne2141 天前
React Native 2025 年度回顾:架构、性能与生态的全面升级
react native·react.js·架构
墨狂之逸才2 天前
React Native 中 Toast 被 react-native-modal 遮挡的解决方案
react native
studyForMokey4 天前
【跨端技术】React Native学习记录一
javascript·学习·react native·react.js
我是刘成4 天前
基于React Native 0.83.1 新架构下的拆包方案
react native·react.js·架构·拆包
全栈前端老曹4 天前
【ReactNative】页面跳转与参数传递 - navigate、push 方法详解
前端·javascript·react native·react.js·页面跳转·移动端开发·页面导航
刘成5 天前
基于React Native 0.83.1 新架构下的拆包方案
react native
2501_916007477 天前
React Native 混淆在真项目中的方式,当 JS 和原生同时暴露
javascript·react native·react.js·ios·小程序·uni-app·iphone
qq_463408427 天前
React Native跨平台技术在开源鸿蒙中使用WebView来加载鸿蒙应用的网页版或通过一个WebView桥接本地代码与鸿蒙应用
javascript·算法·react native·react.js·开源·list·harmonyos