import React, { useCallback, useMemo, useRef } from 'react'; import type { LayoutChangeEvent, StyleProp, TextStyle, ViewStyle, } from 'react-native'; import { Text, View, Animated, Easing, StyleSheet } from 'react-native'; function usePrevious(value: number): number { const ref = React.useRef(value); React.useEffect(() => { ref.current = value; }); return ref.current; } export function createNumberArrayWithCommaAndDecimal(numberString: string): (number | string)[] { const [integerPart, decimalPart] = numberString.split('.'); const integerArr = Array.from(integerPart, Number); const reducedArray = new Array(Math.ceil(integerPart.length / 3)).fill(0); reducedArray.forEach((_, index) => { if (index !== 0) { integerArr.splice(integerPart.length - index * 3, 0, ','); } }); if (decimalPart) { return [...integerArr, '.', ...Array.from(decimalPart, Number)]; } return integerArr; } export interface AnimatedNumberProps { animateToNumber: number; fontStyle?: StyleProp; animationDuration?: number; includeComma?: boolean; decimals?: number; easing?: Animated.TimingAnimationConfig['easing']; containerStyle?: StyleProp; fontVariant?: TextStyle['fontVariant']; } const AnimatedNumber = ({ animateToNumber: givenAnimateToNumber, fontStyle, animationDuration, includeComma, decimals = 2, easing, containerStyle }: AnimatedNumberProps) => { const animationRef = useRef(null); const animateToNumber = parseFloat(givenAnimateToNumber.toFixed(decimals)); const prevNumber = usePrevious(animateToNumber); const animateToNumberString = animateToNumber.toFixed(decimals); const prevNumberString = prevNumber.toFixed(decimals); const NUMBERS = useMemo( () => ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', ','], [] ); const nextNumbersArr = useMemo(() => { return includeComma ? createNumberArrayWithCommaAndDecimal(animateToNumberString) : animateToNumberString.split('').map(char => isNaN(Number(char)) ? char : Number(char)); }, [animateToNumberString, includeComma]); const prevNumbersArr = useMemo(() => { return includeComma ? createNumberArrayWithCommaAndDecimal(prevNumberString) : prevNumberString.split('').map(char => isNaN(Number(char)) ? char : Number(char)); }, [prevNumberString, includeComma]); const [height, setHeight] = React.useState(0); const animations = useMemo( () => height === 0 ? [] : nextNumbersArr.map((__, index) => { const value = prevNumbersArr[index]; if (typeof value !== 'number') { return new Animated.Value(0); } const animationHeight = -1 * (height * value); return new Animated.Value(animationHeight); }), [nextNumbersArr, height, prevNumbersArr] ); const setButtonLayout = useCallback((e: LayoutChangeEvent) => { setHeight(e.nativeEvent.layout.height); }, []); React.useEffect(() => { if (height === 0) return; if (animationRef.current) { animationRef.current.stop(); } const compositions = animations.reduce( (acc, animation, index) => { const value = nextNumbersArr[index]; if (typeof value === 'number') { acc.push( Animated.timing(animation, { toValue: -1 * (height * value), duration: animationDuration || 1400, useNativeDriver: true, easing: easing || Easing.elastic(1.2), }) ); } return acc; }, [] ); animationRef.current = Animated.parallel(compositions); animationRef.current.start(); }, [animations, height, nextNumbersArr, animationDuration, easing]); return ( <> {height !== 0 && ( {animateToNumber < 0 && ( - )} {nextNumbersArr.map((n, index) => { if (typeof n === 'string') { return ( {n} ); } return ( {NUMBERS.map((number, i) => ( {number} ))} ); })} )} {animateToNumberString} ); }; export default AnimatedNumber;