组件概述
基于 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,
);