【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;
相关推荐
wen's8 小时前
React Native 0.79.4 中 [RCTView setColor:] 崩溃问题完整解决方案
javascript·react native·react.js
朝阳3918 小时前
ReactNative【实战系列教程】我的小红书 3 -- 自定义底栏Tab导航(含图片选择 expo-image-picker 的使用)
react native
冰冷的bin21 小时前
【React Native】自定义倒计时组件CountdownView
react native
朝阳3911 天前
React Native【实用教程】(含图标方案,常用第三库,动画,内置组件,内置Hooks,内置API,自定义组件,创建项目等)
react native
朝阳3911 天前
React Native【实战范例】同步跟随滚动
react native
朝阳3913 天前
React Native【详解】动画
react native
朝阳3914 天前
React Native【详解】内置 API
react native
xx240614 天前
React Native学习笔记
笔记·学习·react native
朝阳3914 天前
React Native【实战范例】弹跳动画菜单导航
react native
草明15 天前
解决: React Native iOS webview 空白页
react native·react.js·ios