Last active
August 2, 2024 07:29
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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