react-native-glitter 1.0.1 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/lib/module/index.js +81 -72
- package/lib/typescript/src/index.d.ts +9 -1
- package/package.json +1 -1
- package/src/index.tsx +173 -112
package/README.md
CHANGED
|
@@ -165,7 +165,11 @@ function ControlledGlitter() {
|
|
|
165
165
|
| `position` | `'top' \| 'center' \| 'bottom'` | `'center'` | Position where the line shrinks/expands (for shrink/expand modes) |
|
|
166
166
|
| `direction` | `'left-to-right' \| 'right-to-left'` | `'left-to-right'` | Direction of the shimmer animation |
|
|
167
167
|
| `iterations` | `number` | `-1` | Number of animation cycles (-1 for infinite) |
|
|
168
|
+
| `onAnimationStart` | `() => void` | - | Callback when animation starts |
|
|
168
169
|
| `onAnimationComplete` | `() => void` | - | Callback when all iterations complete |
|
|
170
|
+
| `testID` | `string` | - | Test ID for e2e testing |
|
|
171
|
+
| `accessibilityLabel` | `string` | - | Accessibility label for screen readers |
|
|
172
|
+
| `accessible` | `boolean` | `true` | Whether the component is accessible |
|
|
169
173
|
|
|
170
174
|
## Examples
|
|
171
175
|
|
|
@@ -245,6 +249,7 @@ function ControlledGlitter() {
|
|
|
245
249
|
// Run 3 times then call onAnimationComplete
|
|
246
250
|
<Glitter
|
|
247
251
|
iterations={3}
|
|
252
|
+
onAnimationStart={() => console.log('Started!')}
|
|
248
253
|
onAnimationComplete={() => console.log('Done!')}
|
|
249
254
|
>
|
|
250
255
|
<View style={styles.box} />
|
package/lib/module/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useRef, useState, useCallback, } from 'react';
|
|
2
|
+
import { useEffect, useRef, useState, useCallback, useMemo, memo, } from 'react';
|
|
3
3
|
import { View, Animated, StyleSheet, Easing, } from 'react-native';
|
|
4
4
|
function generateGlitterOpacities(count, peak = 1) {
|
|
5
5
|
const opacities = [];
|
|
@@ -48,49 +48,68 @@ function generateVerticalSegments(fadeRatioParam) {
|
|
|
48
48
|
}
|
|
49
49
|
return segments;
|
|
50
50
|
}
|
|
51
|
-
|
|
51
|
+
const HEIGHT_MULTIPLIER = 1.5;
|
|
52
|
+
const NORMAL_FADE_RATIO = (HEIGHT_MULTIPLIER - 1) / HEIGHT_MULTIPLIER / 2;
|
|
53
|
+
const ANIMATED_SEGMENTS = generateVerticalSegments(0.25);
|
|
54
|
+
const NORMAL_SEGMENTS = generateVerticalSegments(NORMAL_FADE_RATIO);
|
|
55
|
+
function GlitterComponent({ children, duration = 1500, delay = 400, color = 'rgba(255, 255, 255, 0.8)', angle = 20, shimmerWidth = 60, active = true, style, easing, mode = 'normal', position = 'center', direction = 'left-to-right', iterations = -1, onAnimationStart, onAnimationComplete, testID, accessibilityLabel, accessible = true, }) {
|
|
52
56
|
const animatedValue = useRef(new Animated.Value(0)).current;
|
|
53
57
|
const [containerWidth, setContainerWidth] = useState(0);
|
|
54
58
|
const [containerHeight, setContainerHeight] = useState(0);
|
|
55
59
|
const animationRef = useRef(null);
|
|
60
|
+
const currentIterationRef = useRef(null);
|
|
56
61
|
const iterationCount = useRef(0);
|
|
57
|
-
const
|
|
62
|
+
const isAnimatingRef = useRef(false);
|
|
63
|
+
const defaultEasing = useMemo(() => Easing.bezier(0.4, 0, 0.2, 1), []);
|
|
64
|
+
const stopAnimation = useCallback(() => {
|
|
65
|
+
isAnimatingRef.current = false;
|
|
66
|
+
animationRef.current?.stop();
|
|
67
|
+
animationRef.current = null;
|
|
68
|
+
currentIterationRef.current?.stop();
|
|
69
|
+
currentIterationRef.current = null;
|
|
70
|
+
}, []);
|
|
58
71
|
const startAnimation = useCallback(() => {
|
|
59
72
|
if (!active || containerWidth === 0)
|
|
60
73
|
return;
|
|
74
|
+
stopAnimation();
|
|
61
75
|
animatedValue.setValue(0);
|
|
62
76
|
iterationCount.current = 0;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
77
|
+
isAnimatingRef.current = true;
|
|
78
|
+
onAnimationStart?.();
|
|
79
|
+
const createSingleIteration = () => Animated.sequence([
|
|
80
|
+
Animated.timing(animatedValue, {
|
|
81
|
+
toValue: 1,
|
|
82
|
+
duration,
|
|
83
|
+
useNativeDriver: true,
|
|
84
|
+
easing: easing ?? defaultEasing,
|
|
85
|
+
}),
|
|
86
|
+
Animated.delay(delay),
|
|
87
|
+
]);
|
|
70
88
|
const runIteration = () => {
|
|
89
|
+
if (!isAnimatingRef.current)
|
|
90
|
+
return;
|
|
71
91
|
animatedValue.setValue(0);
|
|
72
|
-
|
|
73
|
-
|
|
92
|
+
currentIterationRef.current = createSingleIteration();
|
|
93
|
+
currentIterationRef.current.start(({ finished }) => {
|
|
94
|
+
if (finished && isAnimatingRef.current) {
|
|
74
95
|
iterationCount.current += 1;
|
|
75
96
|
if (iterations === -1 || iterationCount.current < iterations) {
|
|
76
97
|
runIteration();
|
|
77
98
|
}
|
|
78
99
|
else {
|
|
100
|
+
isAnimatingRef.current = false;
|
|
79
101
|
onAnimationComplete?.();
|
|
80
102
|
}
|
|
81
103
|
}
|
|
82
104
|
});
|
|
83
105
|
};
|
|
84
106
|
if (iterations === -1) {
|
|
85
|
-
animationRef.current = Animated.loop(
|
|
107
|
+
animationRef.current = Animated.loop(createSingleIteration());
|
|
86
108
|
animationRef.current.start();
|
|
87
109
|
}
|
|
88
110
|
else {
|
|
89
111
|
runIteration();
|
|
90
112
|
}
|
|
91
|
-
return () => {
|
|
92
|
-
animationRef.current?.stop();
|
|
93
|
-
};
|
|
94
113
|
}, [
|
|
95
114
|
active,
|
|
96
115
|
containerWidth,
|
|
@@ -100,28 +119,29 @@ export function Glitter({ children, duration = 1500, delay = 400, color = 'rgba(
|
|
|
100
119
|
easing,
|
|
101
120
|
defaultEasing,
|
|
102
121
|
iterations,
|
|
122
|
+
onAnimationStart,
|
|
103
123
|
onAnimationComplete,
|
|
124
|
+
stopAnimation,
|
|
104
125
|
]);
|
|
105
126
|
useEffect(() => {
|
|
106
|
-
|
|
107
|
-
return
|
|
108
|
-
}, [startAnimation]);
|
|
127
|
+
startAnimation();
|
|
128
|
+
return stopAnimation;
|
|
129
|
+
}, [startAnimation, stopAnimation]);
|
|
109
130
|
const onLayout = useCallback((event) => {
|
|
110
131
|
const { width, height } = event.nativeEvent.layout;
|
|
111
132
|
setContainerWidth(width);
|
|
112
133
|
setContainerHeight(height);
|
|
113
134
|
}, []);
|
|
114
|
-
const extraWidth = Math.tan((angle * Math.PI) / 180) * 200;
|
|
135
|
+
const extraWidth = useMemo(() => Math.tan((angle * Math.PI) / 180) * 200, [angle]);
|
|
115
136
|
const isLeftToRight = direction === 'left-to-right';
|
|
116
|
-
const translateX = animatedValue.interpolate({
|
|
137
|
+
const translateX = useMemo(() => animatedValue.interpolate({
|
|
117
138
|
inputRange: [0, 1],
|
|
118
139
|
outputRange: isLeftToRight
|
|
119
140
|
? [-shimmerWidth - extraWidth, containerWidth + shimmerWidth]
|
|
120
141
|
: [containerWidth + shimmerWidth, -shimmerWidth - extraWidth],
|
|
121
|
-
});
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
const getScaleY = () => {
|
|
142
|
+
}), [animatedValue, isLeftToRight, shimmerWidth, extraWidth, containerWidth]);
|
|
143
|
+
const lineHeight = containerHeight * HEIGHT_MULTIPLIER;
|
|
144
|
+
const scaleY = useMemo(() => {
|
|
125
145
|
if (mode === 'normal') {
|
|
126
146
|
return 1;
|
|
127
147
|
}
|
|
@@ -135,62 +155,50 @@ export function Glitter({ children, duration = 1500, delay = 400, color = 'rgba(
|
|
|
135
155
|
inputRange: [0, 1],
|
|
136
156
|
outputRange: [1, 0.01],
|
|
137
157
|
});
|
|
138
|
-
};
|
|
158
|
+
}, [mode, animatedValue]);
|
|
139
159
|
const halfHeight = lineHeight / 2;
|
|
140
|
-
const
|
|
141
|
-
const getTransformOriginOffset = () => {
|
|
160
|
+
const transformOriginOffset = useMemo(() => {
|
|
142
161
|
if (mode === 'normal' || position === 'center') {
|
|
143
162
|
return 0;
|
|
144
163
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
};
|
|
152
|
-
const layerCount = Math.max(11, Math.round(shimmerWidth / 3));
|
|
153
|
-
const horizontalOpacities = generateGlitterOpacities(layerCount, 1);
|
|
154
|
-
const layerWidth = shimmerWidth / layerCount;
|
|
155
|
-
const normalFadeRatio = (heightMultiplier - 1) / heightMultiplier / 2;
|
|
156
|
-
const normalSegments = generateVerticalSegments(normalFadeRatio);
|
|
157
|
-
const animatedSegments = generateVerticalSegments(0.25);
|
|
158
|
-
const shimmerLayers = horizontalOpacities.map((opacity, index) => ({
|
|
164
|
+
return position === 'top' ? -halfHeight : halfHeight;
|
|
165
|
+
}, [mode, position, halfHeight]);
|
|
166
|
+
const layerCount = useMemo(() => Math.max(11, Math.round(shimmerWidth / 3)), [shimmerWidth]);
|
|
167
|
+
const layerWidth = useMemo(() => shimmerWidth / layerCount, [shimmerWidth, layerCount]);
|
|
168
|
+
const horizontalOpacities = useMemo(() => generateGlitterOpacities(layerCount, 1), [layerCount]);
|
|
169
|
+
const shimmerLayers = useMemo(() => horizontalOpacities.map((opacity, index) => ({
|
|
159
170
|
opacity,
|
|
160
171
|
position: index * layerWidth - shimmerWidth / 2 + layerWidth / 2,
|
|
161
|
-
}));
|
|
162
|
-
const scaleY = getScaleY();
|
|
163
|
-
const transformOriginOffset = getTransformOriginOffset();
|
|
172
|
+
})), [horizontalOpacities, layerWidth, shimmerWidth]);
|
|
164
173
|
const isAnimated = mode !== 'normal';
|
|
165
|
-
|
|
166
|
-
|
|
174
|
+
const segments = isAnimated ? ANIMATED_SEGMENTS : NORMAL_SEGMENTS;
|
|
175
|
+
const shimmerContainerStyle = useMemo(() => [styles.shimmerContainer, { transform: [{ translateX }] }], [translateX]);
|
|
176
|
+
const rotationWrapperStyle = useMemo(() => [
|
|
177
|
+
styles.rotationWrapper,
|
|
178
|
+
{
|
|
179
|
+
width: shimmerWidth,
|
|
180
|
+
height: lineHeight,
|
|
181
|
+
transform: [{ skewX: `${-angle}deg` }],
|
|
182
|
+
},
|
|
183
|
+
], [shimmerWidth, lineHeight, angle]);
|
|
184
|
+
const getShimmerLineStyle = useCallback((layerPosition) => [
|
|
185
|
+
styles.shimmerLine,
|
|
186
|
+
{
|
|
187
|
+
width: layerWidth + 0.5,
|
|
188
|
+
height: lineHeight,
|
|
189
|
+
left: layerPosition,
|
|
190
|
+
transform: isAnimated
|
|
191
|
+
? [
|
|
192
|
+
{ translateY: transformOriginOffset },
|
|
167
193
|
{
|
|
168
|
-
|
|
194
|
+
scaleY: scaleY,
|
|
169
195
|
},
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
},
|
|
177
|
-
], children: shimmerLayers.map((layer, layerIndex) => (_jsx(Animated.View, { style: [
|
|
178
|
-
styles.shimmerLine,
|
|
179
|
-
{
|
|
180
|
-
width: layerWidth + 0.5,
|
|
181
|
-
height: lineHeight,
|
|
182
|
-
left: layer.position,
|
|
183
|
-
transform: isAnimated
|
|
184
|
-
? [
|
|
185
|
-
{ translateY: startOffset + transformOriginOffset },
|
|
186
|
-
{
|
|
187
|
-
scaleY: scaleY,
|
|
188
|
-
},
|
|
189
|
-
{ translateY: -transformOriginOffset },
|
|
190
|
-
]
|
|
191
|
-
: [{ translateY: startOffset }],
|
|
192
|
-
},
|
|
193
|
-
], children: (isAnimated ? animatedSegments : normalSegments).map((segment, vIndex) => (_jsx(View, { style: [
|
|
196
|
+
{ translateY: -transformOriginOffset },
|
|
197
|
+
]
|
|
198
|
+
: [],
|
|
199
|
+
},
|
|
200
|
+
], [layerWidth, lineHeight, isAnimated, transformOriginOffset, scaleY]);
|
|
201
|
+
return (_jsxs(View, { style: [styles.container, style], onLayout: onLayout, testID: testID, accessibilityLabel: accessibilityLabel, accessible: accessible, children: [children, active && containerWidth > 0 && containerHeight > 0 && (_jsx(Animated.View, { style: shimmerContainerStyle, pointerEvents: "none", children: _jsx(View, { style: rotationWrapperStyle, children: shimmerLayers.map((layer, layerIndex) => (_jsx(Animated.View, { style: getShimmerLineStyle(layer.position), children: segments.map((segment, vIndex) => (_jsx(View, { style: [
|
|
194
202
|
styles.segment,
|
|
195
203
|
{
|
|
196
204
|
height: lineHeight * segment.heightRatio,
|
|
@@ -223,4 +231,5 @@ const styles = StyleSheet.create({
|
|
|
223
231
|
width: '100%',
|
|
224
232
|
},
|
|
225
233
|
});
|
|
234
|
+
export const Glitter = memo(GlitterComponent);
|
|
226
235
|
export default Glitter;
|
|
@@ -17,7 +17,15 @@ export interface GlitterProps {
|
|
|
17
17
|
position?: GlitterPosition;
|
|
18
18
|
direction?: GlitterDirection;
|
|
19
19
|
iterations?: number;
|
|
20
|
+
onAnimationStart?: () => void;
|
|
20
21
|
onAnimationComplete?: () => void;
|
|
22
|
+
/** Test ID for e2e testing */
|
|
23
|
+
testID?: string;
|
|
24
|
+
/** Accessibility label for screen readers */
|
|
25
|
+
accessibilityLabel?: string;
|
|
26
|
+
/** Whether the component is accessible (default: true) */
|
|
27
|
+
accessible?: boolean;
|
|
21
28
|
}
|
|
22
|
-
|
|
29
|
+
declare function GlitterComponent({ children, duration, delay, color, angle, shimmerWidth, active, style, easing, mode, position, direction, iterations, onAnimationStart, onAnimationComplete, testID, accessibilityLabel, accessible, }: GlitterProps): ReactElement;
|
|
30
|
+
export declare const Glitter: import("react").MemoExoticComponent<typeof GlitterComponent>;
|
|
23
31
|
export default Glitter;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-glitter",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "A beautiful shimmer/glitter effect component for React Native. Add a sparkling diagonal shine animation to any component!",
|
|
5
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"types": "./lib/typescript/src/index.d.ts",
|
package/src/index.tsx
CHANGED
|
@@ -3,6 +3,8 @@ import {
|
|
|
3
3
|
useRef,
|
|
4
4
|
useState,
|
|
5
5
|
useCallback,
|
|
6
|
+
useMemo,
|
|
7
|
+
memo,
|
|
6
8
|
type ReactNode,
|
|
7
9
|
type ReactElement,
|
|
8
10
|
} from 'react';
|
|
@@ -36,7 +38,14 @@ export interface GlitterProps {
|
|
|
36
38
|
position?: GlitterPosition;
|
|
37
39
|
direction?: GlitterDirection;
|
|
38
40
|
iterations?: number;
|
|
41
|
+
onAnimationStart?: () => void;
|
|
39
42
|
onAnimationComplete?: () => void;
|
|
43
|
+
/** Test ID for e2e testing */
|
|
44
|
+
testID?: string;
|
|
45
|
+
/** Accessibility label for screen readers */
|
|
46
|
+
accessibilityLabel?: string;
|
|
47
|
+
/** Whether the component is accessible (default: true) */
|
|
48
|
+
accessible?: boolean;
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
function generateGlitterOpacities(count: number, peak: number = 1): number[] {
|
|
@@ -100,7 +109,12 @@ function generateVerticalSegments(fadeRatioParam?: number): VerticalSegment[] {
|
|
|
100
109
|
return segments;
|
|
101
110
|
}
|
|
102
111
|
|
|
103
|
-
|
|
112
|
+
const HEIGHT_MULTIPLIER = 1.5;
|
|
113
|
+
const NORMAL_FADE_RATIO = (HEIGHT_MULTIPLIER - 1) / HEIGHT_MULTIPLIER / 2;
|
|
114
|
+
const ANIMATED_SEGMENTS = generateVerticalSegments(0.25);
|
|
115
|
+
const NORMAL_SEGMENTS = generateVerticalSegments(NORMAL_FADE_RATIO);
|
|
116
|
+
|
|
117
|
+
function GlitterComponent({
|
|
104
118
|
children,
|
|
105
119
|
duration = 1500,
|
|
106
120
|
delay = 400,
|
|
@@ -114,57 +128,79 @@ export function Glitter({
|
|
|
114
128
|
position = 'center',
|
|
115
129
|
direction = 'left-to-right',
|
|
116
130
|
iterations = -1,
|
|
131
|
+
onAnimationStart,
|
|
117
132
|
onAnimationComplete,
|
|
133
|
+
testID,
|
|
134
|
+
accessibilityLabel,
|
|
135
|
+
accessible = true,
|
|
118
136
|
}: GlitterProps): ReactElement {
|
|
119
137
|
const animatedValue = useRef(new Animated.Value(0)).current;
|
|
120
138
|
const [containerWidth, setContainerWidth] = useState(0);
|
|
121
139
|
const [containerHeight, setContainerHeight] = useState(0);
|
|
122
|
-
const animationRef = useRef<
|
|
123
|
-
|
|
124
|
-
|
|
140
|
+
const animationRef = useRef<ReturnType<typeof Animated.loop> | null>(null);
|
|
141
|
+
const currentIterationRef = useRef<ReturnType<
|
|
142
|
+
typeof Animated.sequence
|
|
143
|
+
> | null>(null);
|
|
125
144
|
const iterationCount = useRef(0);
|
|
145
|
+
const isAnimatingRef = useRef(false);
|
|
126
146
|
|
|
127
|
-
const defaultEasing = Easing.bezier(0.4, 0, 0.2, 1);
|
|
147
|
+
const defaultEasing = useMemo(() => Easing.bezier(0.4, 0, 0.2, 1), []);
|
|
148
|
+
|
|
149
|
+
const stopAnimation = useCallback(() => {
|
|
150
|
+
isAnimatingRef.current = false;
|
|
151
|
+
animationRef.current?.stop();
|
|
152
|
+
animationRef.current = null;
|
|
153
|
+
currentIterationRef.current?.stop();
|
|
154
|
+
currentIterationRef.current = null;
|
|
155
|
+
}, []);
|
|
128
156
|
|
|
129
157
|
const startAnimation = useCallback(() => {
|
|
130
158
|
if (!active || containerWidth === 0) return;
|
|
131
159
|
|
|
160
|
+
stopAnimation();
|
|
132
161
|
animatedValue.setValue(0);
|
|
133
162
|
iterationCount.current = 0;
|
|
163
|
+
isAnimatingRef.current = true;
|
|
134
164
|
|
|
135
|
-
|
|
136
|
-
toValue: 1,
|
|
137
|
-
duration,
|
|
138
|
-
useNativeDriver: true,
|
|
139
|
-
easing: easing ?? defaultEasing,
|
|
140
|
-
});
|
|
165
|
+
onAnimationStart?.();
|
|
141
166
|
|
|
142
|
-
const
|
|
167
|
+
const createSingleIteration = () =>
|
|
168
|
+
Animated.sequence([
|
|
169
|
+
Animated.timing(animatedValue, {
|
|
170
|
+
toValue: 1,
|
|
171
|
+
duration,
|
|
172
|
+
useNativeDriver: true,
|
|
173
|
+
easing: easing ?? defaultEasing,
|
|
174
|
+
}),
|
|
175
|
+
Animated.delay(delay),
|
|
176
|
+
]);
|
|
143
177
|
|
|
144
178
|
const runIteration = () => {
|
|
179
|
+
if (!isAnimatingRef.current) return;
|
|
180
|
+
|
|
145
181
|
animatedValue.setValue(0);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
182
|
+
currentIterationRef.current = createSingleIteration();
|
|
183
|
+
currentIterationRef.current.start(
|
|
184
|
+
({ finished }: { finished: boolean }) => {
|
|
185
|
+
if (finished && isAnimatingRef.current) {
|
|
186
|
+
iterationCount.current += 1;
|
|
187
|
+
if (iterations === -1 || iterationCount.current < iterations) {
|
|
188
|
+
runIteration();
|
|
189
|
+
} else {
|
|
190
|
+
isAnimatingRef.current = false;
|
|
191
|
+
onAnimationComplete?.();
|
|
192
|
+
}
|
|
153
193
|
}
|
|
154
194
|
}
|
|
155
|
-
|
|
195
|
+
);
|
|
156
196
|
};
|
|
157
197
|
|
|
158
198
|
if (iterations === -1) {
|
|
159
|
-
animationRef.current = Animated.loop(
|
|
199
|
+
animationRef.current = Animated.loop(createSingleIteration());
|
|
160
200
|
animationRef.current.start();
|
|
161
201
|
} else {
|
|
162
202
|
runIteration();
|
|
163
203
|
}
|
|
164
|
-
|
|
165
|
-
return () => {
|
|
166
|
-
animationRef.current?.stop();
|
|
167
|
-
};
|
|
168
204
|
}, [
|
|
169
205
|
active,
|
|
170
206
|
containerWidth,
|
|
@@ -174,13 +210,15 @@ export function Glitter({
|
|
|
174
210
|
easing,
|
|
175
211
|
defaultEasing,
|
|
176
212
|
iterations,
|
|
213
|
+
onAnimationStart,
|
|
177
214
|
onAnimationComplete,
|
|
215
|
+
stopAnimation,
|
|
178
216
|
]);
|
|
179
217
|
|
|
180
218
|
useEffect(() => {
|
|
181
|
-
|
|
182
|
-
return
|
|
183
|
-
}, [startAnimation]);
|
|
219
|
+
startAnimation();
|
|
220
|
+
return stopAnimation;
|
|
221
|
+
}, [startAnimation, stopAnimation]);
|
|
184
222
|
|
|
185
223
|
const onLayout = useCallback((event: LayoutChangeEvent) => {
|
|
186
224
|
const { width, height } = event.nativeEvent.layout;
|
|
@@ -188,20 +226,29 @@ export function Glitter({
|
|
|
188
226
|
setContainerHeight(height);
|
|
189
227
|
}, []);
|
|
190
228
|
|
|
191
|
-
const extraWidth =
|
|
229
|
+
const extraWidth = useMemo(
|
|
230
|
+
() => Math.tan((angle * Math.PI) / 180) * 200,
|
|
231
|
+
[angle]
|
|
232
|
+
);
|
|
192
233
|
|
|
193
234
|
const isLeftToRight = direction === 'left-to-right';
|
|
194
|
-
const translateX = animatedValue.interpolate({
|
|
195
|
-
inputRange: [0, 1],
|
|
196
|
-
outputRange: isLeftToRight
|
|
197
|
-
? [-shimmerWidth - extraWidth, containerWidth + shimmerWidth]
|
|
198
|
-
: [containerWidth + shimmerWidth, -shimmerWidth - extraWidth],
|
|
199
|
-
});
|
|
200
235
|
|
|
201
|
-
const
|
|
202
|
-
|
|
236
|
+
const translateX = useMemo(
|
|
237
|
+
() =>
|
|
238
|
+
animatedValue.interpolate({
|
|
239
|
+
inputRange: [0, 1],
|
|
240
|
+
outputRange: isLeftToRight
|
|
241
|
+
? [-shimmerWidth - extraWidth, containerWidth + shimmerWidth]
|
|
242
|
+
: [containerWidth + shimmerWidth, -shimmerWidth - extraWidth],
|
|
243
|
+
}),
|
|
244
|
+
[animatedValue, isLeftToRight, shimmerWidth, extraWidth, containerWidth]
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
const lineHeight = containerHeight * HEIGHT_MULTIPLIER;
|
|
203
248
|
|
|
204
|
-
const
|
|
249
|
+
const scaleY = useMemo(():
|
|
250
|
+
| Animated.AnimatedInterpolation<number>
|
|
251
|
+
| number => {
|
|
205
252
|
if (mode === 'normal') {
|
|
206
253
|
return 1;
|
|
207
254
|
}
|
|
@@ -217,100 +264,112 @@ export function Glitter({
|
|
|
217
264
|
inputRange: [0, 1],
|
|
218
265
|
outputRange: [1, 0.01],
|
|
219
266
|
});
|
|
220
|
-
};
|
|
267
|
+
}, [mode, animatedValue]);
|
|
221
268
|
|
|
222
269
|
const halfHeight = lineHeight / 2;
|
|
223
|
-
const startOffset = 0;
|
|
224
270
|
|
|
225
|
-
const
|
|
271
|
+
const transformOriginOffset = useMemo((): number => {
|
|
226
272
|
if (mode === 'normal' || position === 'center') {
|
|
227
273
|
return 0;
|
|
228
274
|
}
|
|
275
|
+
return position === 'top' ? -halfHeight : halfHeight;
|
|
276
|
+
}, [mode, position, halfHeight]);
|
|
229
277
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
};
|
|
278
|
+
const layerCount = useMemo(
|
|
279
|
+
() => Math.max(11, Math.round(shimmerWidth / 3)),
|
|
280
|
+
[shimmerWidth]
|
|
281
|
+
);
|
|
236
282
|
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
283
|
+
const layerWidth = useMemo(
|
|
284
|
+
() => shimmerWidth / layerCount,
|
|
285
|
+
[shimmerWidth, layerCount]
|
|
286
|
+
);
|
|
240
287
|
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
288
|
+
const horizontalOpacities = useMemo(
|
|
289
|
+
() => generateGlitterOpacities(layerCount, 1),
|
|
290
|
+
[layerCount]
|
|
291
|
+
);
|
|
244
292
|
|
|
245
|
-
const shimmerLayers =
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
293
|
+
const shimmerLayers = useMemo(
|
|
294
|
+
() =>
|
|
295
|
+
horizontalOpacities.map((opacity, index) => ({
|
|
296
|
+
opacity,
|
|
297
|
+
position: index * layerWidth - shimmerWidth / 2 + layerWidth / 2,
|
|
298
|
+
})),
|
|
299
|
+
[horizontalOpacities, layerWidth, shimmerWidth]
|
|
300
|
+
);
|
|
249
301
|
|
|
250
|
-
const scaleY = getScaleY();
|
|
251
|
-
const transformOriginOffset = getTransformOriginOffset();
|
|
252
302
|
const isAnimated = mode !== 'normal';
|
|
303
|
+
const segments = isAnimated ? ANIMATED_SEGMENTS : NORMAL_SEGMENTS;
|
|
304
|
+
|
|
305
|
+
const shimmerContainerStyle = useMemo(
|
|
306
|
+
() => [styles.shimmerContainer, { transform: [{ translateX }] }],
|
|
307
|
+
[translateX]
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
const rotationWrapperStyle = useMemo(
|
|
311
|
+
() => [
|
|
312
|
+
styles.rotationWrapper,
|
|
313
|
+
{
|
|
314
|
+
width: shimmerWidth,
|
|
315
|
+
height: lineHeight,
|
|
316
|
+
transform: [{ skewX: `${-angle}deg` }],
|
|
317
|
+
},
|
|
318
|
+
],
|
|
319
|
+
[shimmerWidth, lineHeight, angle]
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
const getShimmerLineStyle = useCallback(
|
|
323
|
+
(layerPosition: number) => [
|
|
324
|
+
styles.shimmerLine,
|
|
325
|
+
{
|
|
326
|
+
width: layerWidth + 0.5,
|
|
327
|
+
height: lineHeight,
|
|
328
|
+
left: layerPosition,
|
|
329
|
+
transform: isAnimated
|
|
330
|
+
? [
|
|
331
|
+
{ translateY: transformOriginOffset },
|
|
332
|
+
{
|
|
333
|
+
scaleY: scaleY as Animated.AnimatedInterpolation<number>,
|
|
334
|
+
},
|
|
335
|
+
{ translateY: -transformOriginOffset },
|
|
336
|
+
]
|
|
337
|
+
: [],
|
|
338
|
+
},
|
|
339
|
+
],
|
|
340
|
+
[layerWidth, lineHeight, isAnimated, transformOriginOffset, scaleY]
|
|
341
|
+
);
|
|
253
342
|
|
|
254
343
|
return (
|
|
255
|
-
<View
|
|
344
|
+
<View
|
|
345
|
+
style={[styles.container, style]}
|
|
346
|
+
onLayout={onLayout}
|
|
347
|
+
testID={testID}
|
|
348
|
+
accessibilityLabel={accessibilityLabel}
|
|
349
|
+
accessible={accessible}
|
|
350
|
+
>
|
|
256
351
|
{children}
|
|
257
352
|
{active && containerWidth > 0 && containerHeight > 0 && (
|
|
258
|
-
<Animated.View
|
|
259
|
-
style={
|
|
260
|
-
styles.shimmerContainer,
|
|
261
|
-
{
|
|
262
|
-
transform: [{ translateX }],
|
|
263
|
-
},
|
|
264
|
-
]}
|
|
265
|
-
pointerEvents="none"
|
|
266
|
-
>
|
|
267
|
-
<View
|
|
268
|
-
style={[
|
|
269
|
-
styles.rotationWrapper,
|
|
270
|
-
{
|
|
271
|
-
width: shimmerWidth,
|
|
272
|
-
height: lineHeight,
|
|
273
|
-
transform: [{ skewX: `${-angle}deg` }],
|
|
274
|
-
},
|
|
275
|
-
]}
|
|
276
|
-
>
|
|
353
|
+
<Animated.View style={shimmerContainerStyle} pointerEvents="none">
|
|
354
|
+
<View style={rotationWrapperStyle}>
|
|
277
355
|
{shimmerLayers.map((layer, layerIndex) => (
|
|
278
356
|
<Animated.View
|
|
279
357
|
key={layerIndex}
|
|
280
|
-
style={
|
|
281
|
-
styles.shimmerLine,
|
|
282
|
-
{
|
|
283
|
-
width: layerWidth + 0.5,
|
|
284
|
-
height: lineHeight,
|
|
285
|
-
left: layer.position,
|
|
286
|
-
transform: isAnimated
|
|
287
|
-
? [
|
|
288
|
-
{ translateY: startOffset + transformOriginOffset },
|
|
289
|
-
{
|
|
290
|
-
scaleY:
|
|
291
|
-
scaleY as Animated.AnimatedInterpolation<number>,
|
|
292
|
-
},
|
|
293
|
-
{ translateY: -transformOriginOffset },
|
|
294
|
-
]
|
|
295
|
-
: [{ translateY: startOffset }],
|
|
296
|
-
},
|
|
297
|
-
]}
|
|
358
|
+
style={getShimmerLineStyle(layer.position)}
|
|
298
359
|
>
|
|
299
|
-
{(
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
)
|
|
313
|
-
)}
|
|
360
|
+
{segments.map((segment, vIndex) => (
|
|
361
|
+
<View
|
|
362
|
+
key={vIndex}
|
|
363
|
+
style={[
|
|
364
|
+
styles.segment,
|
|
365
|
+
{
|
|
366
|
+
height: lineHeight * segment.heightRatio,
|
|
367
|
+
backgroundColor: color,
|
|
368
|
+
opacity: layer.opacity * segment.opacity,
|
|
369
|
+
},
|
|
370
|
+
]}
|
|
371
|
+
/>
|
|
372
|
+
))}
|
|
314
373
|
</Animated.View>
|
|
315
374
|
))}
|
|
316
375
|
</View>
|
|
@@ -345,4 +404,6 @@ const styles = StyleSheet.create({
|
|
|
345
404
|
},
|
|
346
405
|
});
|
|
347
406
|
|
|
407
|
+
export const Glitter = memo(GlitterComponent);
|
|
408
|
+
|
|
348
409
|
export default Glitter;
|