【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;
相关推荐
墨渊君3 天前
React Native 入门指南: 构建 UI 的必备核心组件
前端·react native·react.js
这个昵称也不能用吗?6 天前
react-native搭建开发环境过程记录
前端·react native·cocoapods
厨猿加加6 天前
FlatList 在 React Native 的最佳实践
前端·react native
jinzunqinjiu6 天前
学习react-native组件 1 Image加载图片的组件。
前端·react native
乐影7 天前
React Native踩坑记录之——屏幕适配
前端·react native
就是我8 天前
如何用lazy+ Suspense实现组件延迟加载
javascript·react native·react.js
就是我10 天前
React Native如何避免掉帧
前端·react native·react.js
墨渊君10 天前
React Native 与 React(Web) 开发的不同点, 如何快速上手?
前端·javascript·react native
前端熊猫11 天前
React Native (RN)的学习上手教程
学习·react native·react.js