【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;
相关推荐
赵大仁4 小时前
React Native 与 Expo
javascript·react native·react.js
程序猿阿伟1 天前
《React Native与Flutter:社交应用中用户行为分析与埋点统计的深度剖析》
flutter·react native·react.js
EndingCoder3 天前
跨平台移动开发框架React Native和Flutter性能对比
flutter·react native·react.js
恋猫de小郭4 天前
React Native 前瞻式重大更新 Skia & WebGPU & ThreeJS,未来可期
android·javascript·flutter·react native·react.js·ios
zwjapple5 天前
“ES7+ React/Redux/React-Native snippets“常用快捷前缀
javascript·react native·react.js
程序猿阿伟6 天前
《探索React Native社交应用中WebRTC实现低延迟音视频通话的奥秘》
react native·音视频·webrtc
十步杀一人_千里不留行7 天前
【实战教程】React Native项目集成Google ML Kit实现离线水表OCR识别
react native·react.js·ocr
程序猿阿伟7 天前
《社交应用架构生存战:React Native与Flutter的部署容灾决胜法则》
flutter·react native·架构
流星雨在线8 天前
react naive 网络框架源码解析
网络·react native
老猿阿浪9 天前
突破测试环境文件上传带宽瓶颈!React Native 阿里云 OSS 直传文件格式问题攻克一
react native·阿里云