89 lines
2.4 KiB
TypeScript
89 lines
2.4 KiB
TypeScript
import React, { useEffect, useRef, useState } from 'react';
|
||
import { Animated, Easing, TextStyle } from 'react-native';
|
||
|
||
type AnimatedNumberProps = {
|
||
value: number; // 最终值
|
||
durationMs?: number;
|
||
format?: (v: number) => string;
|
||
style?: TextStyle;
|
||
/** 当该值变化时,从0重新动画 */
|
||
resetToken?: unknown;
|
||
};
|
||
|
||
export function AnimatedNumber({
|
||
value,
|
||
durationMs = 300,
|
||
format,
|
||
style,
|
||
resetToken,
|
||
}: AnimatedNumberProps) {
|
||
const opacity = useRef(new Animated.Value(1)).current;
|
||
const [display, setDisplay] = useState<string>(() =>
|
||
format ? format(value) : `${Math.round(value)}`
|
||
);
|
||
const [currentValue, setCurrentValue] = useState(value);
|
||
const [lastResetToken, setLastResetToken] = useState(resetToken);
|
||
const [isAnimating, setIsAnimating] = useState(false);
|
||
|
||
useEffect(() => {
|
||
// 检查是否需要触发动画
|
||
const valueChanged = value !== currentValue;
|
||
const resetTokenChanged = resetToken !== lastResetToken;
|
||
|
||
// 如果值没有变化且resetToken也没有变化,或者正在动画中,则不执行新动画
|
||
if ((!valueChanged && !resetTokenChanged) || isAnimating) {
|
||
return;
|
||
}
|
||
|
||
// 标记开始动画
|
||
setIsAnimating(true);
|
||
|
||
// 停止当前动画
|
||
opacity.stopAnimation(() => {
|
||
// 创建优雅的透明度变化动画
|
||
const fadeOut = Animated.timing(opacity, {
|
||
toValue: 0.2, // 淡出到较低透明度
|
||
duration: durationMs * 0.4, // 淡出占总时长的40%
|
||
easing: Easing.out(Easing.quad),
|
||
useNativeDriver: false,
|
||
});
|
||
|
||
const fadeIn = Animated.timing(opacity, {
|
||
toValue: 1,
|
||
duration: durationMs * 0.6, // 淡入占总时长的60%
|
||
easing: Easing.out(Easing.quad),
|
||
useNativeDriver: false,
|
||
});
|
||
|
||
// 在淡出完成时更新数字显示
|
||
fadeOut.start(() => {
|
||
// 更新当前值和显示
|
||
setCurrentValue(value);
|
||
setLastResetToken(resetToken);
|
||
setDisplay(format ? format(value) : `${Math.round(value)}`);
|
||
|
||
// 然后淡入新数字
|
||
fadeIn.start(() => {
|
||
// 动画完成,标记结束
|
||
setIsAnimating(false);
|
||
});
|
||
});
|
||
});
|
||
}, [value, resetToken, currentValue, lastResetToken, isAnimating, durationMs, format]);
|
||
|
||
return (
|
||
<Animated.Text
|
||
style={[
|
||
style,
|
||
{
|
||
opacity: opacity
|
||
}
|
||
]}
|
||
>
|
||
{display}
|
||
</Animated.Text>
|
||
);
|
||
}
|
||
|
||
|