【React Native】文本跑马灯组件MarqueeText

组件概述

基于 React Native 的通用水平跑马灯组件,支持以下特性:

  • 文本滚动:支持自定义文本内容
  • 样式自定义:可配置文本样式和容器样式
  • 滚动控制:支持速度、循环、延迟等参数调节
  • 命令式控制:提供 start/stop 方法控制动画
  • 智能适配:自动检测文本/容器尺寸动态调整动画

属性说明

属性名 类型 默认值 描述
text string | undefined - 必填,需要滚动的文本内容
textStyle TextStyle - 文本样式(可覆盖默认样式)
duration number 1000 单次滚动时长(毫秒)
loop boolean true 是否循环播放
delay number 0 动画启动延迟(毫秒)
style StyleProp - 容器样式
children ReactNode - 子组件(暂未使用,保留扩展性)

使用示例

typescript 复制代码
import MarqueeText from './MarqueeText';

// 基础用法
<MarqueeText
  text="这是一个水平跑马灯示例"
  textStyle={{fontSize: 18, color: 'red'}}
  duration={2000}
  loop={true}
  delay={500}
/>

// 自定义样式
<MarqueeText
  text="循环滚动公告"
  style={{height: 40, backgroundColor: '#f0f0f0'}}
  textStyle={{fontWeight: 'bold'}}
  duration={3000}
  loop={true}
/>

API 参考

命令式控制方法

通过 ref 获取组件实例:

typescript 复制代码
const marqueeRef = useRef<MarqueeTextHandles>(null);

// 启动动画
marqueeRef.current?.start();

// 停止动画
marqueeRef.current?.stop();

源码

typescript 复制代码
import React, {
  Ref,
  useCallback,
  useEffect,
  useImperativeHandle,
  useRef,
  useState,
} from 'react';
import {
  Animated,
  Easing,
  findNodeHandle,
  ScrollView,
  StyleProp,
  Text,
  TextStyle,
  UIManager,
  View,
  ViewProps,
} from 'react-native';

export interface MarqueeTextProps extends ViewProps {
  /**
   * 滚动文本
   */
  text: string | undefined;
  /**
   * 文本样式
   */
  textStyle?: StyleProp<TextStyle>;
  /**
   * 滚动时间(毫秒)
   */
  duration?: number;
  /**
   * 是否循环播放滚动动画
   */
  loop?: boolean;
  /**
   * 动画开始前的延迟时间(毫秒)
   */
  delay?: number;
}

export interface MarqueeTextHandles {
  start: () => void; // 启动滚动动画方法
  stop: () => void; // 停止滚动动画方法
}

/**
 * 创建动画配置(支持循环和连续动画)
 * @param animValue 动画值对象
 * @param config 动画配置(目标值、持续时间、是否循环、延迟时间)
 * @returns 组合动画对象
 */
const createAnim = (
  animValue: Animated.Value,
  config: {
    toValue: number;
    duration: number;
    loop: boolean;
    delay: number;
  },
): Animated.CompositeAnimation => {
  // 动画(线性缓动,原生驱动)
  const anim = Animated.timing(animValue, {
    easing: Easing.linear,
    useNativeDriver: true,
    ...config,
  });

  if (config.loop) {
    // 循环(动画完成后延迟1秒重复)
    return Animated.loop(Animated.sequence([anim]));
  }

  return anim; // 单次动画
};

// 跑马灯组件实现
const MarqueeText = (
  props: MarqueeTextProps,
  ref: Ref<MarqueeTextHandles>, // 暴露给父组件的句柄引用
) => {
  // 解构组件属性(带默认值)
  const {
    style,
    text,
    textStyle,
    duration = 1000, // 默认滚动速度1秒
    loop = true, // 默认循环播放
    delay = 0, // 默认无延迟
    children, // 子组件内容
    ...restProps // 其他传递属性
  } = props;

  // 状态:是否正在动画中
  const [isRunning, setIsRunning] = useState<boolean>(false);
  // 缓存:文本(初始为null)
  const textWidth = useRef<number | null>(null);
  // 缓存:容器宽度(初始为null)
  const outWidth = useRef<number | null>(null);
  // 缓存:跑马灯内容实际宽度(初始为null)
  const innerViewWidth = useRef<number | null>(null);
  // 动画值(控制跑马灯内容水平位移)
  const animatedValue = useRef<Animated.Value>(new Animated.Value(0));
  // 跑马灯内容引用(用于测量宽度)
  const innerRef = useRef<typeof Animated.View & View>(null);
  // 滚动容器引用(用于测量容器宽度)
  const outRef = useRef<ScrollView>(null);
  // 动画实例引用(用于控制启动/停止)
  const animRef = useRef<Animated.CompositeAnimation>();
  // 配置缓存(避免重复读取props)
  const conf = useRef<{
    duration: number;
    loop: boolean;
    delay: number;
  }>({
    duration,
    loop,
    delay,
  });

  const resetAnim = useCallback(() => {
    animatedValue.current.setValue(0);
  }, []);

  // 停止动画方法
  const stopAnim = useCallback(() => {
    clearSize(); // 清空尺寸缓存(下次需要重新测量)
    setIsRunning(false); // 更新状态
    animRef.current?.stop();
    animRef.current = undefined;
  }, []);

  // 启动动画方法(核心逻辑)
  const startAnim = useCallback(async (): Promise<void> => {
    stopAnim();
    resetAnim();

    await calSize(); // 计算容器和内容的实际宽度

    // 计算需要滚动的距离(内容宽度的一半,因为内容重复了一次)
    let distance = 0;
    // 计算动画时长(根据速度和距离)
    let animDuration = 0;

    if (!outWidth.current || !innerViewWidth.current || !textWidth.current) {
      // 如果宽度缓存未获取到(测量失败)
      return;
    }
    distance = textWidth.current;
    if (textWidth.current <= outWidth.current) {
      // 内容宽度小于容器宽度,不需要滚动
      return;
    }

    setIsRunning(true); // 标记动画开始

    // 创建动画配置(使用循环模式)
    animRef.current = createAnim(animatedValue.current, {
      ...conf.current,
      toValue: -distance, // 目标位移(向左/下滚动内容宽度的一半)
      duration: duration, // 动画时长
    });

    // 启动动画(无完成回调)
    animRef.current.start((): void => {});
  }, [duration, stopAnim, resetAnim]);

  // 暴露命令式句柄给父组件(start/stop方法)
  useImperativeHandle(ref, () => {
    return {
      start: () => {
        startAnim().then(); // 调用启动方法
      },
      stop: () => {
        stopAnim(); // 调用停止方法
      },
    };
  });

  // 副作用:当isStart变化或子组件更新时触发
  useEffect(() => {
    stopAnim(); // 先停止现有动画
    startAnim().then(); // 重新启动动画
  }, [children, startAnim, stopAnim]); // 依赖子组件和动画方法

  // 测量容器和内容宽度的核心方法(异步)
  const calSize = async (): Promise<void> => {
    try {
      // 如果容器或内容引用不存在则返回
      if (!outRef.current || !innerRef.current) {
        return;
      }

      // 通用测量函数(通过UIManager获取组件宽度)
      const measureWidth = (component: ScrollView | View): Promise<number[]> =>
        new Promise(resolve => {
          UIManager.measure(
            findNodeHandle(component) as number, // 获取组件节点句柄
            (_x: number, _y: number, w: number, h: number) => {
              // 测量回调(返回宽度w和高度h)
              return resolve([w, h]); // 解析宽高
            },
          );
        });

      // 并行测量容器宽度和内容宽度和高度
      const [oWidth, iWidth] = await Promise.all([
        ...(await measureWidth(outRef.current)), // 容器宽度和高度
        ...(await measureWidth(innerRef.current)), // 内容实际宽度和高度
      ]);

      // 缓存测量结果
      outWidth.current = oWidth;
      innerViewWidth.current = iWidth;
    } catch (error) {
      console.error(error);
    }
  };

  // 清空尺寸缓存(用于动画停止后重新测量)
  const clearSize = () => {
    outWidth.current = null;
    innerViewWidth.current = null;
  };

  // 组件渲染结构
  return (
    <View style={[{overflow: 'hidden'}, style]}>
      <ScrollView
        ref={outRef} // 绑定容器引用
        showsHorizontalScrollIndicator={false} // 隐藏水平滚动条
        showsVerticalScrollIndicator={false} // 隐藏垂直滚动条
        horizontal={true} // 水平滚动
        scrollEnabled={false} // 禁用用户手动滚动
      >
        <Animated.View
          ref={innerRef} // 绑定内容引用
          {...restProps} // 传递其他属性
          style={[
            {
              display: 'flex',
              flexDirection: 'row', // 子元素横向排列(使内容重复显示)
              transform: [{translateX: animatedValue.current}], // 应用水平位移动画
            },
            {width: '100%'},
          ]}>
          <Text
            onLayout={e => {
              textWidth.current = e.nativeEvent.layout.width;
              startAnim().then();
            }}
            style={textStyle}
            numberOfLines={1}>
            {text}
          </Text>
          <Text
            style={[{opacity: isRunning ? 1 : 0}, textStyle]}
            numberOfLines={1}>
            {text}
          </Text>
        </Animated.View>
      </ScrollView>
    </View>
  );
};

// 导出带ref的组件(支持命令式调用)
export default React.forwardRef<MarqueeTextHandles, MarqueeTextProps>(
  MarqueeText,
);
相关推荐
RoyLin1 天前
TypeScript设计模式:代理模式
前端·后端·typescript
RoyLin1 天前
TypeScript设计模式:状态模式
前端·后端·typescript
RoyLin1 天前
TypeScript设计模式:观察者模式
前端·后端·typescript
RoyLin1 天前
TypeScript设计模式:备忘录模式
前端·后端·typescript
RoyLin1 天前
TypeScript设计模式:仲裁者模式
前端·后端·typescript
木西2 天前
React Native DApp 开发全栈实战·从 0 到 1 系列(兑换-前端部分)
react native·web3·solidity
RoyLin2 天前
TypeScript设计模式:门面模式
前端·后端·typescript
RoyLin2 天前
TypeScript设计模式:责任链模式
前端·后端·typescript
RoyLin2 天前
TypeScript设计模式:装饰器模式
前端·后端·typescript