Skip to content

Instantly share code, notes, and snippets.

@franko4don
Last active August 2, 2024 07:29
Show Gist options
  • Select an option

  • Save franko4don/171f1beaf3cf8624f3c35ec97b8a8cc9 to your computer and use it in GitHub Desktop.

Select an option

Save franko4don/171f1beaf3cf8624f3c35ec97b8a8cc9 to your computer and use it in GitHub Desktop.
This is a stripped down version of https://www.npmjs.com/package/react-native-animated-numbers but with support for decimal numbers.
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<number>(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<TextStyle>;
animationDuration?: number;
includeComma?: boolean;
decimals?: number;
easing?: Animated.TimingAnimationConfig['easing'];
containerStyle?: StyleProp<ViewStyle>;
fontVariant?: TextStyle['fontVariant'];
}
const AnimatedNumber = ({
animateToNumber: givenAnimateToNumber,
fontStyle,
animationDuration,
includeComma,
decimals = 2,
easing,
containerStyle
}: AnimatedNumberProps) => {
const animationRef = useRef<Animated.CompositeAnimation | null>(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<Animated.CompositeAnimation[]>(
(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 && (
<View
style={StyleSheet.flatten([
containerStyle,
{ flexDirection: 'row', height },
])}
>
{animateToNumber < 0 && (
<Text style={[fontStyle, { height }]}>-</Text>
)}
{nextNumbersArr.map((n, index) => {
if (typeof n === 'string') {
return (
<Text key={index} style={[fontStyle, { height }]}>
{n}
</Text>
);
}
return (
<View key={index} style={{ height, overflow: 'hidden' }}>
<Animated.View
style={[
{
transform: [
{
translateY: animations[index]!,
},
],
},
]}
>
{NUMBERS.map((number, i) => (
<Text
style={StyleSheet.flatten([
fontStyle,
{ height },
])}
key={i}
>
{number}
</Text>
))}
</Animated.View>
</View>
);
})}
</View>
)}
<View style={{ opacity: 0, position: 'absolute' }} pointerEvents="none">
<Text style={fontStyle} onLayout={setButtonLayout} numberOfLines={1}>
{animateToNumberString}
</Text>
</View>
</>
);
};
export default AnimatedNumber;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment