react-native-slot-text 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- package/AnimatedNumbers.d.ts +18 -0
- package/AnimatedNumbers.js +147 -0
- package/LICENSE +21 -0
- package/README.md +45 -0
- package/Slot.d.ts +3 -0
- package/Slot.js +27 -0
- package/helpers.d.ts +1 -0
- package/helpers.js +1 -0
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/package.json +37 -0
- package/styles.d.ts +18 -0
- package/styles.js +19 -0
- package/types.d.ts +19 -0
- package/types.js +1 -0
@@ -0,0 +1,18 @@
|
|
1
|
+
import type { AnimatedNumbersProps } from './types';
|
2
|
+
/**
|
3
|
+
* AnimatedNumbers Component
|
4
|
+
*
|
5
|
+
* This component animates numeric values, transitioning between old and new numbers.
|
6
|
+
* It supports animation of individual digits, optional commas, and a customizable prefix.
|
7
|
+
* The animation occurs over a defined duration and can be repeated as the value changes.
|
8
|
+
*
|
9
|
+
* @param {number|string} props.value - The value to animate to. Can be string of numbers.
|
10
|
+
* @param {Object} props.fontStyle - The style of the text, passed as a TextStyle object.
|
11
|
+
* @param {number} [props.animationDuration=200] - The duration of the animation in milliseconds. Defaults to 200ms.
|
12
|
+
* @param {string} [props.prefix=""] - A prefix to the number, such as a currency symbol.
|
13
|
+
* @param {boolean} [props.includeComma=false] - Whether to include commas as thousand separators.
|
14
|
+
*
|
15
|
+
* @returns {JSX.Element} The animated number component with slots for digits and commas.
|
16
|
+
*/
|
17
|
+
declare const AnimatedNumbers: (props: AnimatedNumbersProps) => import("react/jsx-runtime").JSX.Element;
|
18
|
+
export default AnimatedNumbers;
|
@@ -0,0 +1,147 @@
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
2
|
+
import { useEffect, useState, useRef, useCallback } from "react";
|
3
|
+
import { View, Text, Animated } from "react-native";
|
4
|
+
import ReAnimated, { ZoomIn, StretchOutX } from 'react-native-reanimated';
|
5
|
+
import styles from './styles';
|
6
|
+
import Slot from './Slot';
|
7
|
+
import { formatString } from "./helpers";
|
8
|
+
const DEFAULT_DURTION = 200;
|
9
|
+
/**
|
10
|
+
* AnimatedNumbers Component
|
11
|
+
*
|
12
|
+
* This component animates numeric values, transitioning between old and new numbers.
|
13
|
+
* It supports animation of individual digits, optional commas, and a customizable prefix.
|
14
|
+
* The animation occurs over a defined duration and can be repeated as the value changes.
|
15
|
+
*
|
16
|
+
* @param {number|string} props.value - The value to animate to. Can be string of numbers.
|
17
|
+
* @param {Object} props.fontStyle - The style of the text, passed as a TextStyle object.
|
18
|
+
* @param {number} [props.animationDuration=200] - The duration of the animation in milliseconds. Defaults to 200ms.
|
19
|
+
* @param {string} [props.prefix=""] - A prefix to the number, such as a currency symbol.
|
20
|
+
* @param {boolean} [props.includeComma=false] - Whether to include commas as thousand separators.
|
21
|
+
*
|
22
|
+
* @returns {JSX.Element} The animated number component with slots for digits and commas.
|
23
|
+
*/
|
24
|
+
const AnimatedNumbers = (props) => {
|
25
|
+
const [state, setState] = useState('idle');
|
26
|
+
const [oldNumber, setOldNumber] = useState([]);
|
27
|
+
const [newNumber, setNewNumber] = useState([]);
|
28
|
+
const [animatingValue, setAnimatingValue] = useState();
|
29
|
+
// The initial positions of the slots that are animating in
|
30
|
+
// -1 indicates above the slot, 0 indicates the slot, 1 indicates below the slot
|
31
|
+
const [inZeroPositions, setInZeroPositions] = useState([]);
|
32
|
+
// The final positions of the slots animating out
|
33
|
+
const [outFinalPositions, setOutFinalPositions] = useState([]);
|
34
|
+
const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
|
35
|
+
const containerWidth = useRef(new Animated.Value(0)).current;
|
36
|
+
const containerHeight = useRef(new Animated.Value(0)).current;
|
37
|
+
const measureRef = useRef(null);
|
38
|
+
useEffect(() => {
|
39
|
+
setOldNumber(`${props.prefix || ''}${props.value}`
|
40
|
+
.split('')
|
41
|
+
.map((val, i) => isNaN(val) ? val : parseInt(val)));
|
42
|
+
}, []);
|
43
|
+
useEffect(() => {
|
44
|
+
// Animation is in progress, queue the new value
|
45
|
+
if (state === 'idle' && oldNumber.join('') !== formatString(props.value, props.prefix)) {
|
46
|
+
setAnimatingValue(formatString(props.value, props.prefix));
|
47
|
+
}
|
48
|
+
}, [props.value, state]);
|
49
|
+
useEffect(() => {
|
50
|
+
if (!animatingValue)
|
51
|
+
return;
|
52
|
+
setState('animating');
|
53
|
+
// If the old number is not set, this means it's the initial rendering of this component
|
54
|
+
// and the value needs to be loaded
|
55
|
+
if (oldNumber?.length === 0) {
|
56
|
+
setOldNumber(animatingValue
|
57
|
+
.split('')
|
58
|
+
.map((val, i) => isNaN(val) ? val : parseInt(val)));
|
59
|
+
}
|
60
|
+
// Prepare the new number for animation
|
61
|
+
else {
|
62
|
+
setNewNumber(animatingValue
|
63
|
+
.split('')
|
64
|
+
.map((val, i) => isNaN(val) ? val : parseInt(val)));
|
65
|
+
// The positions of the slots that are animating in
|
66
|
+
let newValuePositions = Array(animatingValue.length).fill(0);
|
67
|
+
// Compare from the right to the left
|
68
|
+
// * If the slots are the same value, no animation is needed for the slot, just stick it in the -1 position
|
69
|
+
// * If one of the slots is a string, the new slot will move down
|
70
|
+
// * If the new slot is greater than the old slot, the new slot will move down
|
71
|
+
// or up depending on which is greater
|
72
|
+
for (let i = 0; i < newValuePositions.length; i++) {
|
73
|
+
const oldNum = oldNumber[i];
|
74
|
+
const newNum = isNaN(animatingValue.charAt(i))
|
75
|
+
? animatingValue.charAt(i)
|
76
|
+
: parseInt(animatingValue.charAt(i));
|
77
|
+
if (oldNum === newNum) {
|
78
|
+
newValuePositions[i] = undefined;
|
79
|
+
}
|
80
|
+
else {
|
81
|
+
newValuePositions[i] = typeof oldNum === 'string' || typeof newNum === 'string'
|
82
|
+
? -1
|
83
|
+
: oldNum === undefined ? -1 : newNum > oldNum ? -1 : 1;
|
84
|
+
}
|
85
|
+
}
|
86
|
+
setInZeroPositions(newValuePositions);
|
87
|
+
setOutFinalPositions([
|
88
|
+
...newValuePositions.slice(0, oldNumber.length).map(val => val ? val * -1 : 0),
|
89
|
+
...Array(Math.max(0, oldNumber.length - animatingValue.length)).fill(-1)
|
90
|
+
]);
|
91
|
+
}
|
92
|
+
}, [animatingValue]);
|
93
|
+
const onMeasureLayout = useCallback((e) => {
|
94
|
+
if (containerSize.width === 0) {
|
95
|
+
containerWidth.setValue(e.nativeEvent.layout.width);
|
96
|
+
containerHeight.setValue(e.nativeEvent.layout.height);
|
97
|
+
}
|
98
|
+
else {
|
99
|
+
Animated.timing(containerWidth, {
|
100
|
+
toValue: e.nativeEvent.layout.width,
|
101
|
+
duration: props.animationDuration || DEFAULT_DURTION,
|
102
|
+
useNativeDriver: false,
|
103
|
+
delay: containerSize.width > e.nativeEvent.layout.width ? props.animationDuration || DEFAULT_DURTION : 0
|
104
|
+
}).start();
|
105
|
+
Animated.timing(containerHeight, {
|
106
|
+
toValue: e.nativeEvent.layout.height,
|
107
|
+
duration: props.animationDuration || DEFAULT_DURTION,
|
108
|
+
useNativeDriver: false
|
109
|
+
}).start();
|
110
|
+
}
|
111
|
+
setContainerSize(e.nativeEvent.layout);
|
112
|
+
}, []);
|
113
|
+
const onCompleted = useCallback(() => {
|
114
|
+
setOldNumber(newNumber);
|
115
|
+
setNewNumber([]);
|
116
|
+
setInZeroPositions([]);
|
117
|
+
setOutFinalPositions([]);
|
118
|
+
setState('idle');
|
119
|
+
}, [newNumber]);
|
120
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Animated.View, { style: [
|
121
|
+
{ width: containerWidth, height: containerHeight },
|
122
|
+
styles.animatedNumbers
|
123
|
+
], children: [_jsx(View, { style: styles.slotsContainer, children: oldNumber.map((val, i) => (_jsxs(_Fragment, { children: [(oldNumber.length - i) % 3 === 0 && i > 1 && props.includeComma &&
|
124
|
+
_jsx(ReAnimated.View, { entering: ZoomIn.delay(props.animationDuration || DEFAULT_DURTION).withInitialValues({ opacity: 0 }), exiting: StretchOutX.withInitialValues({ opacity: 1 }), children: _jsx(Text, { style: props.fontStyle, children: "," }) }, `${val}-${i}-comma`), _jsx(Slot, { value: val, height: containerSize.height, initial: 0, final: outFinalPositions[i] || 0, animationDuration: props.animationDuration || DEFAULT_DURTION, fontStyle: props.fontStyle }, `${val}-${i}`)] }))) }), _jsx(View, { style: styles.slotsContainer, children: newNumber.map((val, i) => (_jsxs(_Fragment, { children: [(newNumber.length - i) % 3 === 0 && i > 1 && props.includeComma &&
|
125
|
+
_jsx(ReAnimated.View, { entering: ZoomIn.delay(props.animationDuration || DEFAULT_DURTION).withInitialValues({ opacity: 0 }), exiting: StretchOutX.withInitialValues({ opacity: 1 }), children: _jsx(Text, { style: props.fontStyle, children: "," }) }, `${val}-${i}-comma-new`), _jsx(Slot, { value: val, initial: inZeroPositions[i] || -1, final: inZeroPositions[i] ? 0 : -1, height: containerSize.height, animationDuration: props.animationDuration || DEFAULT_DURTION, fontStyle: props.fontStyle, onCompleted: onCompleted }, `${val}-${i}`)] }))) })] }), _jsxs(View, { onLayout: onMeasureLayout, style: styles.spacer, ref: measureRef, children: [props.prefix && _jsx(Text, { style: props.fontStyle, children: props.prefix }), oldNumber.map((val, i) => (_jsx(Text, { style: props.fontStyle, children: val }, `${val}-${i}`)))] })] }));
|
126
|
+
};
|
127
|
+
/*
|
128
|
+
Basic logic:
|
129
|
+
|
130
|
+
When a new value comes in:
|
131
|
+
|
132
|
+
1. If there is an animation currently in progress then queue the new value with any prefix prepended
|
133
|
+
|
134
|
+
2. For an animation cycle, split the new number and set the new number state
|
135
|
+
|
136
|
+
3. Loop through the new number and old number and compare digits, determing which way the new numbers
|
137
|
+
and old numers need to animate. There are two versions of the number, one that's visible, and one outside
|
138
|
+
the visible clipping container. If a slot has the same number between the old and new nummer, it wont be animated.
|
139
|
+
|
140
|
+
4. Set the new positions wich will trigger the animations of the slots
|
141
|
+
|
142
|
+
5. At the end, clear the new number and set the old number to the new number
|
143
|
+
|
144
|
+
6. Pop any queued values and set the formated value, which will retriger the animation cycle
|
145
|
+
|
146
|
+
*/
|
147
|
+
export default AnimatedNumbers;
|
package/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2020 Geist UI.
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# react-native-slot-text
|
2
|
+
|
3
|
+
To install dependencies:
|
4
|
+
|
5
|
+
```bash
|
6
|
+
bun install react-native-slot-text
|
7
|
+
```
|
8
|
+
|
9
|
+
This project was created using `bun init` in bun v1.1.21. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|
10
|
+
|
11
|
+
https://github.com/user-attachments/assets/df5449f1-e20c-4a18-8687-67f29652d7b6
|
12
|
+
|
13
|
+
|
14
|
+
## Usage
|
15
|
+
|
16
|
+
```
|
17
|
+
<Slider
|
18
|
+
value={value}
|
19
|
+
onValueChange={(value) => setValue(value[0])}
|
20
|
+
minimumValue={0}
|
21
|
+
maximumValue={limit_amount || 100}
|
22
|
+
step={1}
|
23
|
+
maximumTrackTintColor={theme.colors.quinaryText}
|
24
|
+
minimumTrackTintColor={theme.colors.blueText}
|
25
|
+
thumbTintColor={theme.colors.whiteText}
|
26
|
+
thumbStyle={{
|
27
|
+
width: 18,
|
28
|
+
height: 18,
|
29
|
+
shadowColor: theme.colors.navShadow,
|
30
|
+
shadowOffset: { width: 0, height: 1 },
|
31
|
+
shadowOpacity: 1,
|
32
|
+
shadowRadius: 1,
|
33
|
+
}}
|
34
|
+
/>
|
35
|
+
```
|
36
|
+
|
37
|
+
### Props
|
38
|
+
|
39
|
+
| Prop | Type | Default | Description |
|
40
|
+
|---------------------|-----------------------|----------|--------------------------------------------------------------------------------------------------|
|
41
|
+
| `value` | '${number}' | `N/A` | The value to animate to. Can be a number or a string of numbers. |
|
42
|
+
| `fontStyle` | `Object` | `N/A` | The style of the text, passed as a `TextStyle` object. |
|
43
|
+
| `animationDuration` | `number` | `200` | The duration of the animation in milliseconds. Defaults to 200ms. |
|
44
|
+
| `prefix` | `string` | `""` | A prefix to the number, such as a currency symbol. |
|
45
|
+
| `includeComma` | `boolean` | `false` | Whether to include commas as thousand separators. |
|
package/Slot.d.ts
ADDED
package/Slot.js
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
2
|
+
import { useRef, useEffect } from 'react';
|
3
|
+
import { Text, Animated } from 'react-native';
|
4
|
+
const Slot = (props) => {
|
5
|
+
const Y = useRef(new Animated.Value(props.initial * props.height));
|
6
|
+
useEffect(() => {
|
7
|
+
if (props.final !== undefined) {
|
8
|
+
Animated.timing(Y.current, {
|
9
|
+
toValue: props.final * props.height,
|
10
|
+
duration: props.animationDuration,
|
11
|
+
useNativeDriver: true
|
12
|
+
}).start(() => {
|
13
|
+
if (props.onCompleted) {
|
14
|
+
props.onCompleted();
|
15
|
+
}
|
16
|
+
});
|
17
|
+
}
|
18
|
+
}, [props.final]);
|
19
|
+
return (_jsx(Animated.View, { style: [{
|
20
|
+
transform: [{ translateY: Y.current }],
|
21
|
+
opacity: Y.current.interpolate({
|
22
|
+
inputRange: [-1 * props.height, 0, props.height],
|
23
|
+
outputRange: [0, 1, 0]
|
24
|
+
})
|
25
|
+
}], children: _jsx(Text, { style: props.fontStyle, children: props.value }) }));
|
26
|
+
};
|
27
|
+
export default Slot;
|
package/helpers.d.ts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
export declare const formatString: (v: string, p?: string) => string;
|
package/helpers.js
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
export const formatString = (v, p) => `${p || ''}${v}`;
|
package/index.d.ts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
export { default as SlotText } from './AnimatedNumbers';
|
package/index.js
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
export { default as SlotText } from './AnimatedNumbers';
|
package/package.json
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
{
|
2
|
+
"name": "react-native-slot-text",
|
3
|
+
"version": "1.0.0",
|
4
|
+
"author": "sc-mitton <scm.mitton@gmail.com> (https://github.com/sc-mitton)",
|
5
|
+
"main": "index.js",
|
6
|
+
"module": "index.ts",
|
7
|
+
"devDependencies": {
|
8
|
+
"@types/lodash": "^4.17.9",
|
9
|
+
"@types/react": "^18.3.10",
|
10
|
+
"@types/react-native": "^0.73.0",
|
11
|
+
"typescript": "^5.0.0",
|
12
|
+
"tsx": "^4.15.5",
|
13
|
+
"@types/bun": "latest"
|
14
|
+
},
|
15
|
+
"peerDependencies": {
|
16
|
+
"react": "^18.3.1",
|
17
|
+
"react-native": "^0.75.3",
|
18
|
+
"react-native-reanimated": "^3.15.4"
|
19
|
+
},
|
20
|
+
"description": "Slot machine style animted text for React Native",
|
21
|
+
"keywords": [
|
22
|
+
"slot",
|
23
|
+
"text",
|
24
|
+
"react-native",
|
25
|
+
"react",
|
26
|
+
"slot-machine",
|
27
|
+
"react-native-slot-text",
|
28
|
+
"animation"
|
29
|
+
],
|
30
|
+
"license": "MIT",
|
31
|
+
"scripts": {
|
32
|
+
"build": "tsc -p tsconfig.json",
|
33
|
+
"release": "tsx src/pre-release.ts && cd lib && npm publish --access public --no-git-checks"
|
34
|
+
},
|
35
|
+
"type": "module",
|
36
|
+
"types": "src/types.d.ts"
|
37
|
+
}
|
package/styles.d.ts
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
declare const styles: {
|
2
|
+
animatedNumbers: {
|
3
|
+
overflow: "hidden";
|
4
|
+
};
|
5
|
+
spacer: {
|
6
|
+
position: "absolute";
|
7
|
+
flexDirection: "row";
|
8
|
+
opacity: number;
|
9
|
+
gap: number;
|
10
|
+
};
|
11
|
+
slotsContainer: {
|
12
|
+
position: "absolute";
|
13
|
+
flexDirection: "row";
|
14
|
+
justifyContent: "center";
|
15
|
+
gap: number;
|
16
|
+
};
|
17
|
+
};
|
18
|
+
export default styles;
|
package/styles.js
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
import { StyleSheet } from 'react-native';
|
2
|
+
const styles = StyleSheet.create({
|
3
|
+
animatedNumbers: {
|
4
|
+
overflow: 'hidden',
|
5
|
+
},
|
6
|
+
spacer: {
|
7
|
+
position: 'absolute',
|
8
|
+
flexDirection: 'row',
|
9
|
+
opacity: 0,
|
10
|
+
gap: 2
|
11
|
+
},
|
12
|
+
slotsContainer: {
|
13
|
+
position: 'absolute',
|
14
|
+
flexDirection: 'row',
|
15
|
+
justifyContent: 'center',
|
16
|
+
gap: 2,
|
17
|
+
}
|
18
|
+
});
|
19
|
+
export default styles;
|
package/types.d.ts
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
import type { StyleProp, TextStyle } from 'react-native';
|
2
|
+
export type Position = -1 | 0 | 1;
|
3
|
+
export interface SlotProps {
|
4
|
+
value: number | string;
|
5
|
+
animationDuration: number;
|
6
|
+
fontStyle?: StyleProp<TextStyle>;
|
7
|
+
initial: Position;
|
8
|
+
final: Position;
|
9
|
+
height: number;
|
10
|
+
isNew?: boolean;
|
11
|
+
onCompleted?: () => void;
|
12
|
+
}
|
13
|
+
export interface AnimatedNumbersProps {
|
14
|
+
value: `${number}`;
|
15
|
+
fontStyle?: StyleProp<TextStyle>;
|
16
|
+
animationDuration?: number;
|
17
|
+
prefix?: string;
|
18
|
+
includeComma?: boolean;
|
19
|
+
}
|
package/types.js
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
import { Text } from 'react-native';
|