【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;
相关推荐
哼唧唧_15 小时前
React Native开发鸿蒙运动健康类应用的项目实践记录
react native·harmonyos·harmony os5·运动健康
程序员小刘1 天前
鸿蒙【HarmonyOS 5】 (React Native)的实战教程
react native·华为·harmonyos
烈焰晴天1 天前
使用ReactNative加载Svga动画支持三端【Android/IOS/Harmony】
android·react native·ios
哼唧唧_2 天前
使用 React Native 开发鸿蒙运动健康类应用的高频易错点总结
react native·react.js·harmonyos·harmony os5·运动健康
鄂鱼2 天前
使用react-native-skia实现自绘地图实践与踩坑记录
react native
依旧002 天前
react native webview加载本地HTML,解决iOS无法加载成功问题
react native·ios·html·vite·webview
不想当reducer2 天前
React Native 阿里云 OSS 上传实战:从相册资源处理到动态签名管理
前端·javascript·react native
hellokai3 天前
ReactNative介绍及简化版原理实现
android·react native
哼唧唧_4 天前
基于React Native开发鸿蒙新闻类应用的实战开发笔记
react native·华为·新闻·harmony os5
老猿阿浪4 天前
React Native 实现抖音式图片滑动切换浏览组件-媲美抖音体验的滑动式流畅预览组件
javascript·react native·react.js