react-native-glitter 1.0.4 → 1.0.6
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 +40 -0
- package/lib/module/index.js +91 -6
- package/lib/typescript/src/index.d.ts +193 -6
- package/package.json +1 -12
- package/src/index.tsx +336 -51
package/README.md
CHANGED
|
@@ -170,6 +170,7 @@ function ControlledGlitter() {
|
|
|
170
170
|
| `testID` | `string` | - | Test ID for e2e testing |
|
|
171
171
|
| `accessibilityLabel` | `string` | - | Accessibility label for screen readers |
|
|
172
172
|
| `accessible` | `boolean` | `true` | Whether the component is accessible |
|
|
173
|
+
| `respectReduceMotion` | `boolean` | `true` | Whether to respect the system's "Reduce Motion" setting |
|
|
173
174
|
|
|
174
175
|
## Examples
|
|
175
176
|
|
|
@@ -264,6 +265,44 @@ function ControlledGlitter() {
|
|
|
264
265
|
</Glitter>
|
|
265
266
|
```
|
|
266
267
|
|
|
268
|
+
## Ref API
|
|
269
|
+
|
|
270
|
+
You can control the animation programmatically using a ref:
|
|
271
|
+
|
|
272
|
+
```tsx
|
|
273
|
+
import { useRef } from 'react';
|
|
274
|
+
import { Glitter, type GlitterRef } from 'react-native-glitter';
|
|
275
|
+
|
|
276
|
+
function MyComponent() {
|
|
277
|
+
const glitterRef = useRef<GlitterRef>(null);
|
|
278
|
+
|
|
279
|
+
const handleStart = () => glitterRef.current?.start();
|
|
280
|
+
const handleStop = () => glitterRef.current?.stop();
|
|
281
|
+
const handleRestart = () => glitterRef.current?.restart();
|
|
282
|
+
const checkStatus = () => console.log(glitterRef.current?.isAnimating());
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<>
|
|
286
|
+
<Glitter ref={glitterRef} active={false}>
|
|
287
|
+
<View style={styles.box} />
|
|
288
|
+
</Glitter>
|
|
289
|
+
<Button title="Start" onPress={handleStart} />
|
|
290
|
+
<Button title="Stop" onPress={handleStop} />
|
|
291
|
+
<Button title="Restart" onPress={handleRestart} />
|
|
292
|
+
</>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Ref Methods
|
|
298
|
+
|
|
299
|
+
| Method | Return | Description |
|
|
300
|
+
|--------|--------|-------------|
|
|
301
|
+
| `start()` | `void` | Start the shimmer animation |
|
|
302
|
+
| `stop()` | `void` | Stop the shimmer animation |
|
|
303
|
+
| `restart()` | `void` | Restart the animation from the beginning |
|
|
304
|
+
| `isAnimating()` | `boolean` | Check if animation is currently running |
|
|
305
|
+
|
|
267
306
|
## TypeScript
|
|
268
307
|
|
|
269
308
|
This library is written in TypeScript and includes type definitions:
|
|
@@ -272,6 +311,7 @@ This library is written in TypeScript and includes type definitions:
|
|
|
272
311
|
import {
|
|
273
312
|
Glitter,
|
|
274
313
|
type GlitterProps,
|
|
314
|
+
type GlitterRef,
|
|
275
315
|
type GlitterMode,
|
|
276
316
|
type GlitterPosition,
|
|
277
317
|
type GlitterDirection,
|
package/lib/module/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useRef, useState, useCallback, useMemo, memo, } from 'react';
|
|
3
|
-
import { View, Animated, StyleSheet, Easing, } from 'react-native';
|
|
2
|
+
import { useEffect, useRef, useState, useCallback, useMemo, memo, useImperativeHandle, forwardRef, } from 'react';
|
|
3
|
+
import { View, Animated, StyleSheet, Easing, AccessibilityInfo, } from 'react-native';
|
|
4
4
|
function generateGlitterOpacities(count, peak = 1) {
|
|
5
5
|
const opacities = [];
|
|
6
6
|
const center = (count - 1) / 2;
|
|
@@ -52,10 +52,37 @@ const HEIGHT_MULTIPLIER = 1.5;
|
|
|
52
52
|
const NORMAL_FADE_RATIO = (HEIGHT_MULTIPLIER - 1) / HEIGHT_MULTIPLIER / 2;
|
|
53
53
|
const ANIMATED_SEGMENTS = generateVerticalSegments(0.25);
|
|
54
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, }) {
|
|
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, respectReduceMotion = true, }, ref) {
|
|
56
56
|
const animatedValue = useRef(new Animated.Value(0)).current;
|
|
57
57
|
const [containerWidth, setContainerWidth] = useState(0);
|
|
58
58
|
const [containerHeight, setContainerHeight] = useState(0);
|
|
59
|
+
const [reduceMotionEnabled, setReduceMotionEnabled] = useState(false);
|
|
60
|
+
// Detect system reduce motion preference
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!respectReduceMotion) {
|
|
63
|
+
setReduceMotionEnabled(false);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
let isMounted = true;
|
|
67
|
+
AccessibilityInfo.isReduceMotionEnabled()
|
|
68
|
+
.then((enabled) => {
|
|
69
|
+
if (isMounted) {
|
|
70
|
+
setReduceMotionEnabled(enabled);
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
.catch(() => {
|
|
74
|
+
// Ignore errors (e.g., on web where this might not be supported)
|
|
75
|
+
});
|
|
76
|
+
const subscription = AccessibilityInfo.addEventListener('reduceMotionChanged', (enabled) => {
|
|
77
|
+
if (isMounted) {
|
|
78
|
+
setReduceMotionEnabled(enabled);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
return () => {
|
|
82
|
+
isMounted = false;
|
|
83
|
+
subscription.remove();
|
|
84
|
+
};
|
|
85
|
+
}, [respectReduceMotion]);
|
|
59
86
|
const animationRef = useRef(null);
|
|
60
87
|
const currentIterationRef = useRef(null);
|
|
61
88
|
const iterationCount = useRef(0);
|
|
@@ -68,8 +95,25 @@ function GlitterComponent({ children, duration = 1500, delay = 400, color = 'rgb
|
|
|
68
95
|
currentIterationRef.current?.stop();
|
|
69
96
|
currentIterationRef.current = null;
|
|
70
97
|
}, []);
|
|
98
|
+
const restartAnimation = useCallback(() => {
|
|
99
|
+
stopAnimation();
|
|
100
|
+
animatedValue.setValue(0);
|
|
101
|
+
// Trigger re-render to start animation
|
|
102
|
+
setContainerWidth((prev) => prev);
|
|
103
|
+
}, [stopAnimation, animatedValue]);
|
|
104
|
+
useImperativeHandle(ref, () => ({
|
|
105
|
+
start: () => {
|
|
106
|
+
if (!isAnimatingRef.current && containerWidth > 0) {
|
|
107
|
+
// Force start by triggering the effect
|
|
108
|
+
setContainerWidth((prev) => prev);
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
stop: stopAnimation,
|
|
112
|
+
restart: restartAnimation,
|
|
113
|
+
isAnimating: () => isAnimatingRef.current,
|
|
114
|
+
}), [stopAnimation, restartAnimation, containerWidth]);
|
|
71
115
|
const startAnimation = useCallback(() => {
|
|
72
|
-
if (!active || containerWidth === 0)
|
|
116
|
+
if (!active || containerWidth === 0 || reduceMotionEnabled)
|
|
73
117
|
return;
|
|
74
118
|
stopAnimation();
|
|
75
119
|
animatedValue.setValue(0);
|
|
@@ -122,6 +166,7 @@ function GlitterComponent({ children, duration = 1500, delay = 400, color = 'rgb
|
|
|
122
166
|
onAnimationStart,
|
|
123
167
|
onAnimationComplete,
|
|
124
168
|
stopAnimation,
|
|
169
|
+
reduceMotionEnabled,
|
|
125
170
|
]);
|
|
126
171
|
useEffect(() => {
|
|
127
172
|
startAnimation();
|
|
@@ -198,7 +243,10 @@ function GlitterComponent({ children, duration = 1500, delay = 400, color = 'rgb
|
|
|
198
243
|
: [],
|
|
199
244
|
},
|
|
200
245
|
], [layerWidth, lineHeight, isAnimated, transformOriginOffset, scaleY]);
|
|
201
|
-
return (_jsxs(View, { style: [styles.container, style], onLayout: onLayout, testID: testID, accessibilityLabel: accessibilityLabel, accessible: accessible, children: [children, active &&
|
|
246
|
+
return (_jsxs(View, { style: [styles.container, style], onLayout: onLayout, testID: testID, accessibilityLabel: accessibilityLabel, accessible: accessible, children: [children, active &&
|
|
247
|
+
!reduceMotionEnabled &&
|
|
248
|
+
containerWidth > 0 &&
|
|
249
|
+
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: [
|
|
202
250
|
styles.segment,
|
|
203
251
|
{
|
|
204
252
|
height: lineHeight * segment.heightRatio,
|
|
@@ -231,5 +279,42 @@ const styles = StyleSheet.create({
|
|
|
231
279
|
width: '100%',
|
|
232
280
|
},
|
|
233
281
|
});
|
|
234
|
-
|
|
282
|
+
const ForwardedGlitter = forwardRef(GlitterComponent);
|
|
283
|
+
/**
|
|
284
|
+
* A beautiful shimmer/glitter effect component for React Native.
|
|
285
|
+
* Wrap any component to add a sparkling diagonal shine animation.
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* ```tsx
|
|
289
|
+
* // Basic usage
|
|
290
|
+
* <Glitter>
|
|
291
|
+
* <View style={styles.card}>
|
|
292
|
+
* <Text>This content will shimmer!</Text>
|
|
293
|
+
* </View>
|
|
294
|
+
* </Glitter>
|
|
295
|
+
*
|
|
296
|
+
* // With custom options
|
|
297
|
+
* <Glitter
|
|
298
|
+
* duration={2000}
|
|
299
|
+
* color="rgba(255, 215, 0, 0.5)"
|
|
300
|
+
* mode="expand"
|
|
301
|
+
* direction="right-to-left"
|
|
302
|
+
* >
|
|
303
|
+
* <View style={styles.premiumButton} />
|
|
304
|
+
* </Glitter>
|
|
305
|
+
*
|
|
306
|
+
* // With ref for programmatic control
|
|
307
|
+
* const glitterRef = useRef<GlitterRef>(null);
|
|
308
|
+
* <Glitter ref={glitterRef} active={false}>
|
|
309
|
+
* <View style={styles.box} />
|
|
310
|
+
* </Glitter>
|
|
311
|
+
* // Later: glitterRef.current?.start();
|
|
312
|
+
* ```
|
|
313
|
+
*
|
|
314
|
+
* @see {@link GlitterProps} for available props
|
|
315
|
+
* @see {@link GlitterRef} for ref methods
|
|
316
|
+
*/
|
|
317
|
+
export const Glitter = memo(ForwardedGlitter);
|
|
318
|
+
// Set display name for React DevTools
|
|
319
|
+
Glitter.displayName = 'Glitter';
|
|
235
320
|
export default Glitter;
|
|
@@ -1,31 +1,218 @@
|
|
|
1
|
-
import { type ReactNode
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
2
|
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
/**
|
|
4
|
+
* Animation mode for the shimmer effect.
|
|
5
|
+
* - `normal`: Constant size shimmer line
|
|
6
|
+
* - `expand`: Shimmer line starts small and grows
|
|
7
|
+
* - `shrink`: Shimmer line starts full size and shrinks
|
|
8
|
+
*/
|
|
3
9
|
export type GlitterMode = 'normal' | 'expand' | 'shrink';
|
|
10
|
+
/**
|
|
11
|
+
* Position where the shimmer line shrinks to or expands from.
|
|
12
|
+
* Only applies when mode is 'expand' or 'shrink'.
|
|
13
|
+
* - `top`: Shrinks to/expands from the top
|
|
14
|
+
* - `center`: Shrinks to/expands from the center
|
|
15
|
+
* - `bottom`: Shrinks to/expands from the bottom
|
|
16
|
+
*/
|
|
4
17
|
export type GlitterPosition = 'top' | 'center' | 'bottom';
|
|
18
|
+
/**
|
|
19
|
+
* Direction of the shimmer animation movement.
|
|
20
|
+
* - `left-to-right`: Shimmer moves from left to right
|
|
21
|
+
* - `right-to-left`: Shimmer moves from right to left
|
|
22
|
+
*/
|
|
5
23
|
export type GlitterDirection = 'left-to-right' | 'right-to-left';
|
|
24
|
+
/**
|
|
25
|
+
* Props for the Glitter component.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```tsx
|
|
29
|
+
* <Glitter
|
|
30
|
+
* duration={1500}
|
|
31
|
+
* color="rgba(255, 255, 255, 0.8)"
|
|
32
|
+
* mode="expand"
|
|
33
|
+
* >
|
|
34
|
+
* <View style={styles.card} />
|
|
35
|
+
* </Glitter>
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
6
38
|
export interface GlitterProps {
|
|
39
|
+
/**
|
|
40
|
+
* The content to apply the shimmer effect to.
|
|
41
|
+
* Can be any valid React node.
|
|
42
|
+
*/
|
|
7
43
|
children: ReactNode;
|
|
44
|
+
/**
|
|
45
|
+
* Duration of one shimmer animation cycle in milliseconds.
|
|
46
|
+
* @default 1500
|
|
47
|
+
*/
|
|
8
48
|
duration?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Delay between animation cycles in milliseconds.
|
|
51
|
+
* @default 400
|
|
52
|
+
*/
|
|
9
53
|
delay?: number;
|
|
54
|
+
/**
|
|
55
|
+
* Color of the shimmer effect.
|
|
56
|
+
* Supports any valid React Native color format (rgba, hex, rgb, named colors).
|
|
57
|
+
* @default 'rgba(255, 255, 255, 0.8)'
|
|
58
|
+
* @example 'rgba(255, 215, 0, 0.5)' // Gold shimmer
|
|
59
|
+
*/
|
|
10
60
|
color?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Angle of the shimmer in degrees.
|
|
63
|
+
* 0 = horizontal, 45 = diagonal.
|
|
64
|
+
* @default 20
|
|
65
|
+
*/
|
|
11
66
|
angle?: number;
|
|
67
|
+
/**
|
|
68
|
+
* Width of the shimmer band in pixels.
|
|
69
|
+
* @default 60
|
|
70
|
+
*/
|
|
12
71
|
shimmerWidth?: number;
|
|
72
|
+
/**
|
|
73
|
+
* Whether the animation is active.
|
|
74
|
+
* Set to false to pause the animation.
|
|
75
|
+
* @default true
|
|
76
|
+
*/
|
|
13
77
|
active?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Additional styles for the container View.
|
|
80
|
+
*/
|
|
14
81
|
style?: StyleProp<ViewStyle>;
|
|
82
|
+
/**
|
|
83
|
+
* Custom easing function for the animation.
|
|
84
|
+
* If not provided, uses a smooth bezier curve (0.4, 0, 0.2, 1).
|
|
85
|
+
* @param value - Input value between 0 and 1
|
|
86
|
+
* @returns Output value between 0 and 1
|
|
87
|
+
* @example (value) => value * value // Ease in quad
|
|
88
|
+
*/
|
|
15
89
|
easing?: (value: number) => number;
|
|
90
|
+
/**
|
|
91
|
+
* Animation mode for the shimmer line.
|
|
92
|
+
* @default 'normal'
|
|
93
|
+
*/
|
|
16
94
|
mode?: GlitterMode;
|
|
95
|
+
/**
|
|
96
|
+
* Position where the line shrinks/expands.
|
|
97
|
+
* Only applies when mode is 'expand' or 'shrink'.
|
|
98
|
+
* @default 'center'
|
|
99
|
+
*/
|
|
17
100
|
position?: GlitterPosition;
|
|
101
|
+
/**
|
|
102
|
+
* Direction of the shimmer animation.
|
|
103
|
+
* @default 'left-to-right'
|
|
104
|
+
*/
|
|
18
105
|
direction?: GlitterDirection;
|
|
106
|
+
/**
|
|
107
|
+
* Number of animation cycles.
|
|
108
|
+
* Set to -1 for infinite loop.
|
|
109
|
+
* @default -1
|
|
110
|
+
*/
|
|
19
111
|
iterations?: number;
|
|
112
|
+
/**
|
|
113
|
+
* Callback fired when the animation starts.
|
|
114
|
+
* Called once at the beginning of the animation sequence.
|
|
115
|
+
*/
|
|
20
116
|
onAnimationStart?: () => void;
|
|
117
|
+
/**
|
|
118
|
+
* Callback fired when all iterations complete.
|
|
119
|
+
* Only called when iterations is a positive number.
|
|
120
|
+
*/
|
|
21
121
|
onAnimationComplete?: () => void;
|
|
22
|
-
/**
|
|
122
|
+
/**
|
|
123
|
+
* Test ID for e2e testing frameworks like Detox.
|
|
124
|
+
*/
|
|
23
125
|
testID?: string;
|
|
24
|
-
/**
|
|
126
|
+
/**
|
|
127
|
+
* Accessibility label for screen readers.
|
|
128
|
+
* Describes the shimmer effect to visually impaired users.
|
|
129
|
+
*/
|
|
25
130
|
accessibilityLabel?: string;
|
|
26
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
* Whether the component is accessible.
|
|
133
|
+
* @default true
|
|
134
|
+
*/
|
|
27
135
|
accessible?: boolean;
|
|
136
|
+
/**
|
|
137
|
+
* Whether to respect the system's "Reduce Motion" accessibility setting.
|
|
138
|
+
* When enabled and the user has reduced motion enabled, the shimmer animation will be disabled.
|
|
139
|
+
* @default true
|
|
140
|
+
*/
|
|
141
|
+
respectReduceMotion?: boolean;
|
|
28
142
|
}
|
|
29
|
-
|
|
30
|
-
|
|
143
|
+
/**
|
|
144
|
+
* Ref methods exposed by the Glitter component for programmatic control.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```tsx
|
|
148
|
+
* const glitterRef = useRef<GlitterRef>(null);
|
|
149
|
+
*
|
|
150
|
+
* // Control animation programmatically
|
|
151
|
+
* glitterRef.current?.start();
|
|
152
|
+
* glitterRef.current?.stop();
|
|
153
|
+
* glitterRef.current?.restart();
|
|
154
|
+
*
|
|
155
|
+
* // Check animation status
|
|
156
|
+
* if (glitterRef.current?.isAnimating()) {
|
|
157
|
+
* console.log('Animation is running');
|
|
158
|
+
* }
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
export interface GlitterRef {
|
|
162
|
+
/**
|
|
163
|
+
* Start the shimmer animation.
|
|
164
|
+
* Has no effect if already animating or container has no size.
|
|
165
|
+
*/
|
|
166
|
+
start: () => void;
|
|
167
|
+
/**
|
|
168
|
+
* Stop the shimmer animation immediately.
|
|
169
|
+
* Cleans up all animation references.
|
|
170
|
+
*/
|
|
171
|
+
stop: () => void;
|
|
172
|
+
/**
|
|
173
|
+
* Restart the shimmer animation from the beginning.
|
|
174
|
+
* Stops current animation and starts fresh.
|
|
175
|
+
*/
|
|
176
|
+
restart: () => void;
|
|
177
|
+
/**
|
|
178
|
+
* Check if animation is currently running.
|
|
179
|
+
* @returns true if animation is active, false otherwise
|
|
180
|
+
*/
|
|
181
|
+
isAnimating: () => boolean;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* A beautiful shimmer/glitter effect component for React Native.
|
|
185
|
+
* Wrap any component to add a sparkling diagonal shine animation.
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```tsx
|
|
189
|
+
* // Basic usage
|
|
190
|
+
* <Glitter>
|
|
191
|
+
* <View style={styles.card}>
|
|
192
|
+
* <Text>This content will shimmer!</Text>
|
|
193
|
+
* </View>
|
|
194
|
+
* </Glitter>
|
|
195
|
+
*
|
|
196
|
+
* // With custom options
|
|
197
|
+
* <Glitter
|
|
198
|
+
* duration={2000}
|
|
199
|
+
* color="rgba(255, 215, 0, 0.5)"
|
|
200
|
+
* mode="expand"
|
|
201
|
+
* direction="right-to-left"
|
|
202
|
+
* >
|
|
203
|
+
* <View style={styles.premiumButton} />
|
|
204
|
+
* </Glitter>
|
|
205
|
+
*
|
|
206
|
+
* // With ref for programmatic control
|
|
207
|
+
* const glitterRef = useRef<GlitterRef>(null);
|
|
208
|
+
* <Glitter ref={glitterRef} active={false}>
|
|
209
|
+
* <View style={styles.box} />
|
|
210
|
+
* </Glitter>
|
|
211
|
+
* // Later: glitterRef.current?.start();
|
|
212
|
+
* ```
|
|
213
|
+
*
|
|
214
|
+
* @see {@link GlitterProps} for available props
|
|
215
|
+
* @see {@link GlitterRef} for ref methods
|
|
216
|
+
*/
|
|
217
|
+
export declare const Glitter: import("react").NamedExoticComponent<GlitterProps & import("react").RefAttributes<GlitterRef>>;
|
|
31
218
|
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.6",
|
|
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",
|
|
@@ -15,17 +15,6 @@
|
|
|
15
15
|
"files": [
|
|
16
16
|
"src",
|
|
17
17
|
"lib",
|
|
18
|
-
"android",
|
|
19
|
-
"ios",
|
|
20
|
-
"cpp",
|
|
21
|
-
"*.podspec",
|
|
22
|
-
"react-native.config.js",
|
|
23
|
-
"!ios/build",
|
|
24
|
-
"!android/build",
|
|
25
|
-
"!android/gradle",
|
|
26
|
-
"!android/gradlew",
|
|
27
|
-
"!android/gradlew.bat",
|
|
28
|
-
"!android/local.properties",
|
|
29
18
|
"!**/__tests__",
|
|
30
19
|
"!**/__fixtures__",
|
|
31
20
|
"!**/__mocks__",
|
package/src/index.tsx
CHANGED
|
@@ -5,47 +5,227 @@ import {
|
|
|
5
5
|
useCallback,
|
|
6
6
|
useMemo,
|
|
7
7
|
memo,
|
|
8
|
+
useImperativeHandle,
|
|
9
|
+
forwardRef,
|
|
8
10
|
type ReactNode,
|
|
9
11
|
type ReactElement,
|
|
12
|
+
type ForwardedRef,
|
|
10
13
|
} from 'react';
|
|
11
14
|
import {
|
|
12
15
|
View,
|
|
13
16
|
Animated,
|
|
14
17
|
StyleSheet,
|
|
15
18
|
Easing,
|
|
19
|
+
AccessibilityInfo,
|
|
16
20
|
type LayoutChangeEvent,
|
|
17
21
|
type StyleProp,
|
|
18
22
|
type ViewStyle,
|
|
19
23
|
} from 'react-native';
|
|
20
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Animation mode for the shimmer effect.
|
|
27
|
+
* - `normal`: Constant size shimmer line
|
|
28
|
+
* - `expand`: Shimmer line starts small and grows
|
|
29
|
+
* - `shrink`: Shimmer line starts full size and shrinks
|
|
30
|
+
*/
|
|
21
31
|
export type GlitterMode = 'normal' | 'expand' | 'shrink';
|
|
22
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Position where the shimmer line shrinks to or expands from.
|
|
35
|
+
* Only applies when mode is 'expand' or 'shrink'.
|
|
36
|
+
* - `top`: Shrinks to/expands from the top
|
|
37
|
+
* - `center`: Shrinks to/expands from the center
|
|
38
|
+
* - `bottom`: Shrinks to/expands from the bottom
|
|
39
|
+
*/
|
|
23
40
|
export type GlitterPosition = 'top' | 'center' | 'bottom';
|
|
24
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Direction of the shimmer animation movement.
|
|
44
|
+
* - `left-to-right`: Shimmer moves from left to right
|
|
45
|
+
* - `right-to-left`: Shimmer moves from right to left
|
|
46
|
+
*/
|
|
25
47
|
export type GlitterDirection = 'left-to-right' | 'right-to-left';
|
|
26
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Props for the Glitter component.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```tsx
|
|
54
|
+
* <Glitter
|
|
55
|
+
* duration={1500}
|
|
56
|
+
* color="rgba(255, 255, 255, 0.8)"
|
|
57
|
+
* mode="expand"
|
|
58
|
+
* >
|
|
59
|
+
* <View style={styles.card} />
|
|
60
|
+
* </Glitter>
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
27
63
|
export interface GlitterProps {
|
|
64
|
+
/**
|
|
65
|
+
* The content to apply the shimmer effect to.
|
|
66
|
+
* Can be any valid React node.
|
|
67
|
+
*/
|
|
28
68
|
children: ReactNode;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Duration of one shimmer animation cycle in milliseconds.
|
|
72
|
+
* @default 1500
|
|
73
|
+
*/
|
|
29
74
|
duration?: number;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Delay between animation cycles in milliseconds.
|
|
78
|
+
* @default 400
|
|
79
|
+
*/
|
|
30
80
|
delay?: number;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Color of the shimmer effect.
|
|
84
|
+
* Supports any valid React Native color format (rgba, hex, rgb, named colors).
|
|
85
|
+
* @default 'rgba(255, 255, 255, 0.8)'
|
|
86
|
+
* @example 'rgba(255, 215, 0, 0.5)' // Gold shimmer
|
|
87
|
+
*/
|
|
31
88
|
color?: string;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Angle of the shimmer in degrees.
|
|
92
|
+
* 0 = horizontal, 45 = diagonal.
|
|
93
|
+
* @default 20
|
|
94
|
+
*/
|
|
32
95
|
angle?: number;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Width of the shimmer band in pixels.
|
|
99
|
+
* @default 60
|
|
100
|
+
*/
|
|
33
101
|
shimmerWidth?: number;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Whether the animation is active.
|
|
105
|
+
* Set to false to pause the animation.
|
|
106
|
+
* @default true
|
|
107
|
+
*/
|
|
34
108
|
active?: boolean;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Additional styles for the container View.
|
|
112
|
+
*/
|
|
35
113
|
style?: StyleProp<ViewStyle>;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Custom easing function for the animation.
|
|
117
|
+
* If not provided, uses a smooth bezier curve (0.4, 0, 0.2, 1).
|
|
118
|
+
* @param value - Input value between 0 and 1
|
|
119
|
+
* @returns Output value between 0 and 1
|
|
120
|
+
* @example (value) => value * value // Ease in quad
|
|
121
|
+
*/
|
|
36
122
|
easing?: (value: number) => number;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Animation mode for the shimmer line.
|
|
126
|
+
* @default 'normal'
|
|
127
|
+
*/
|
|
37
128
|
mode?: GlitterMode;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Position where the line shrinks/expands.
|
|
132
|
+
* Only applies when mode is 'expand' or 'shrink'.
|
|
133
|
+
* @default 'center'
|
|
134
|
+
*/
|
|
38
135
|
position?: GlitterPosition;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Direction of the shimmer animation.
|
|
139
|
+
* @default 'left-to-right'
|
|
140
|
+
*/
|
|
39
141
|
direction?: GlitterDirection;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Number of animation cycles.
|
|
145
|
+
* Set to -1 for infinite loop.
|
|
146
|
+
* @default -1
|
|
147
|
+
*/
|
|
40
148
|
iterations?: number;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Callback fired when the animation starts.
|
|
152
|
+
* Called once at the beginning of the animation sequence.
|
|
153
|
+
*/
|
|
41
154
|
onAnimationStart?: () => void;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Callback fired when all iterations complete.
|
|
158
|
+
* Only called when iterations is a positive number.
|
|
159
|
+
*/
|
|
42
160
|
onAnimationComplete?: () => void;
|
|
43
|
-
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Test ID for e2e testing frameworks like Detox.
|
|
164
|
+
*/
|
|
44
165
|
testID?: string;
|
|
45
|
-
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Accessibility label for screen readers.
|
|
169
|
+
* Describes the shimmer effect to visually impaired users.
|
|
170
|
+
*/
|
|
46
171
|
accessibilityLabel?: string;
|
|
47
|
-
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Whether the component is accessible.
|
|
175
|
+
* @default true
|
|
176
|
+
*/
|
|
48
177
|
accessible?: boolean;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Whether to respect the system's "Reduce Motion" accessibility setting.
|
|
181
|
+
* When enabled and the user has reduced motion enabled, the shimmer animation will be disabled.
|
|
182
|
+
* @default true
|
|
183
|
+
*/
|
|
184
|
+
respectReduceMotion?: boolean;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Ref methods exposed by the Glitter component for programmatic control.
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* ```tsx
|
|
192
|
+
* const glitterRef = useRef<GlitterRef>(null);
|
|
193
|
+
*
|
|
194
|
+
* // Control animation programmatically
|
|
195
|
+
* glitterRef.current?.start();
|
|
196
|
+
* glitterRef.current?.stop();
|
|
197
|
+
* glitterRef.current?.restart();
|
|
198
|
+
*
|
|
199
|
+
* // Check animation status
|
|
200
|
+
* if (glitterRef.current?.isAnimating()) {
|
|
201
|
+
* console.log('Animation is running');
|
|
202
|
+
* }
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
export interface GlitterRef {
|
|
206
|
+
/**
|
|
207
|
+
* Start the shimmer animation.
|
|
208
|
+
* Has no effect if already animating or container has no size.
|
|
209
|
+
*/
|
|
210
|
+
start: () => void;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Stop the shimmer animation immediately.
|
|
214
|
+
* Cleans up all animation references.
|
|
215
|
+
*/
|
|
216
|
+
stop: () => void;
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Restart the shimmer animation from the beginning.
|
|
220
|
+
* Stops current animation and starts fresh.
|
|
221
|
+
*/
|
|
222
|
+
restart: () => void;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Check if animation is currently running.
|
|
226
|
+
* @returns true if animation is active, false otherwise
|
|
227
|
+
*/
|
|
228
|
+
isAnimating: () => boolean;
|
|
49
229
|
}
|
|
50
230
|
|
|
51
231
|
function generateGlitterOpacities(count: number, peak: number = 1): number[] {
|
|
@@ -114,29 +294,68 @@ const NORMAL_FADE_RATIO = (HEIGHT_MULTIPLIER - 1) / HEIGHT_MULTIPLIER / 2;
|
|
|
114
294
|
const ANIMATED_SEGMENTS = generateVerticalSegments(0.25);
|
|
115
295
|
const NORMAL_SEGMENTS = generateVerticalSegments(NORMAL_FADE_RATIO);
|
|
116
296
|
|
|
117
|
-
function GlitterComponent(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
297
|
+
function GlitterComponent(
|
|
298
|
+
{
|
|
299
|
+
children,
|
|
300
|
+
duration = 1500,
|
|
301
|
+
delay = 400,
|
|
302
|
+
color = 'rgba(255, 255, 255, 0.8)',
|
|
303
|
+
angle = 20,
|
|
304
|
+
shimmerWidth = 60,
|
|
305
|
+
active = true,
|
|
306
|
+
style,
|
|
307
|
+
easing,
|
|
308
|
+
mode = 'normal',
|
|
309
|
+
position = 'center',
|
|
310
|
+
direction = 'left-to-right',
|
|
311
|
+
iterations = -1,
|
|
312
|
+
onAnimationStart,
|
|
313
|
+
onAnimationComplete,
|
|
314
|
+
testID,
|
|
315
|
+
accessibilityLabel,
|
|
316
|
+
accessible = true,
|
|
317
|
+
respectReduceMotion = true,
|
|
318
|
+
}: GlitterProps,
|
|
319
|
+
ref: ForwardedRef<GlitterRef>
|
|
320
|
+
): ReactElement {
|
|
137
321
|
const animatedValue = useRef(new Animated.Value(0)).current;
|
|
138
322
|
const [containerWidth, setContainerWidth] = useState(0);
|
|
139
323
|
const [containerHeight, setContainerHeight] = useState(0);
|
|
324
|
+
const [reduceMotionEnabled, setReduceMotionEnabled] = useState(false);
|
|
325
|
+
|
|
326
|
+
// Detect system reduce motion preference
|
|
327
|
+
useEffect(() => {
|
|
328
|
+
if (!respectReduceMotion) {
|
|
329
|
+
setReduceMotionEnabled(false);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
let isMounted = true;
|
|
334
|
+
|
|
335
|
+
AccessibilityInfo.isReduceMotionEnabled()
|
|
336
|
+
.then((enabled) => {
|
|
337
|
+
if (isMounted) {
|
|
338
|
+
setReduceMotionEnabled(enabled);
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
.catch(() => {
|
|
342
|
+
// Ignore errors (e.g., on web where this might not be supported)
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const subscription = AccessibilityInfo.addEventListener(
|
|
346
|
+
'reduceMotionChanged',
|
|
347
|
+
(enabled) => {
|
|
348
|
+
if (isMounted) {
|
|
349
|
+
setReduceMotionEnabled(enabled);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
return () => {
|
|
355
|
+
isMounted = false;
|
|
356
|
+
subscription.remove();
|
|
357
|
+
};
|
|
358
|
+
}, [respectReduceMotion]);
|
|
140
359
|
const animationRef = useRef<ReturnType<typeof Animated.loop> | null>(null);
|
|
141
360
|
const currentIterationRef = useRef<ReturnType<
|
|
142
361
|
typeof Animated.sequence
|
|
@@ -154,8 +373,31 @@ function GlitterComponent({
|
|
|
154
373
|
currentIterationRef.current = null;
|
|
155
374
|
}, []);
|
|
156
375
|
|
|
376
|
+
const restartAnimation = useCallback(() => {
|
|
377
|
+
stopAnimation();
|
|
378
|
+
animatedValue.setValue(0);
|
|
379
|
+
// Trigger re-render to start animation
|
|
380
|
+
setContainerWidth((prev) => prev);
|
|
381
|
+
}, [stopAnimation, animatedValue]);
|
|
382
|
+
|
|
383
|
+
useImperativeHandle(
|
|
384
|
+
ref,
|
|
385
|
+
() => ({
|
|
386
|
+
start: () => {
|
|
387
|
+
if (!isAnimatingRef.current && containerWidth > 0) {
|
|
388
|
+
// Force start by triggering the effect
|
|
389
|
+
setContainerWidth((prev) => prev);
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
stop: stopAnimation,
|
|
393
|
+
restart: restartAnimation,
|
|
394
|
+
isAnimating: () => isAnimatingRef.current,
|
|
395
|
+
}),
|
|
396
|
+
[stopAnimation, restartAnimation, containerWidth]
|
|
397
|
+
);
|
|
398
|
+
|
|
157
399
|
const startAnimation = useCallback(() => {
|
|
158
|
-
if (!active || containerWidth === 0) return;
|
|
400
|
+
if (!active || containerWidth === 0 || reduceMotionEnabled) return;
|
|
159
401
|
|
|
160
402
|
stopAnimation();
|
|
161
403
|
animatedValue.setValue(0);
|
|
@@ -213,6 +455,7 @@ function GlitterComponent({
|
|
|
213
455
|
onAnimationStart,
|
|
214
456
|
onAnimationComplete,
|
|
215
457
|
stopAnimation,
|
|
458
|
+
reduceMotionEnabled,
|
|
216
459
|
]);
|
|
217
460
|
|
|
218
461
|
useEffect(() => {
|
|
@@ -349,32 +592,35 @@ function GlitterComponent({
|
|
|
349
592
|
accessible={accessible}
|
|
350
593
|
>
|
|
351
594
|
{children}
|
|
352
|
-
{active &&
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
{
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
595
|
+
{active &&
|
|
596
|
+
!reduceMotionEnabled &&
|
|
597
|
+
containerWidth > 0 &&
|
|
598
|
+
containerHeight > 0 && (
|
|
599
|
+
<Animated.View style={shimmerContainerStyle} pointerEvents="none">
|
|
600
|
+
<View style={rotationWrapperStyle}>
|
|
601
|
+
{shimmerLayers.map((layer, layerIndex) => (
|
|
602
|
+
<Animated.View
|
|
603
|
+
key={layerIndex}
|
|
604
|
+
style={getShimmerLineStyle(layer.position)}
|
|
605
|
+
>
|
|
606
|
+
{segments.map((segment, vIndex) => (
|
|
607
|
+
<View
|
|
608
|
+
key={vIndex}
|
|
609
|
+
style={[
|
|
610
|
+
styles.segment,
|
|
611
|
+
{
|
|
612
|
+
height: lineHeight * segment.heightRatio,
|
|
613
|
+
backgroundColor: color,
|
|
614
|
+
opacity: layer.opacity * segment.opacity,
|
|
615
|
+
},
|
|
616
|
+
]}
|
|
617
|
+
/>
|
|
618
|
+
))}
|
|
619
|
+
</Animated.View>
|
|
620
|
+
))}
|
|
621
|
+
</View>
|
|
622
|
+
</Animated.View>
|
|
623
|
+
)}
|
|
378
624
|
</View>
|
|
379
625
|
);
|
|
380
626
|
}
|
|
@@ -404,6 +650,45 @@ const styles = StyleSheet.create({
|
|
|
404
650
|
},
|
|
405
651
|
});
|
|
406
652
|
|
|
407
|
-
|
|
653
|
+
const ForwardedGlitter = forwardRef(GlitterComponent);
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* A beautiful shimmer/glitter effect component for React Native.
|
|
657
|
+
* Wrap any component to add a sparkling diagonal shine animation.
|
|
658
|
+
*
|
|
659
|
+
* @example
|
|
660
|
+
* ```tsx
|
|
661
|
+
* // Basic usage
|
|
662
|
+
* <Glitter>
|
|
663
|
+
* <View style={styles.card}>
|
|
664
|
+
* <Text>This content will shimmer!</Text>
|
|
665
|
+
* </View>
|
|
666
|
+
* </Glitter>
|
|
667
|
+
*
|
|
668
|
+
* // With custom options
|
|
669
|
+
* <Glitter
|
|
670
|
+
* duration={2000}
|
|
671
|
+
* color="rgba(255, 215, 0, 0.5)"
|
|
672
|
+
* mode="expand"
|
|
673
|
+
* direction="right-to-left"
|
|
674
|
+
* >
|
|
675
|
+
* <View style={styles.premiumButton} />
|
|
676
|
+
* </Glitter>
|
|
677
|
+
*
|
|
678
|
+
* // With ref for programmatic control
|
|
679
|
+
* const glitterRef = useRef<GlitterRef>(null);
|
|
680
|
+
* <Glitter ref={glitterRef} active={false}>
|
|
681
|
+
* <View style={styles.box} />
|
|
682
|
+
* </Glitter>
|
|
683
|
+
* // Later: glitterRef.current?.start();
|
|
684
|
+
* ```
|
|
685
|
+
*
|
|
686
|
+
* @see {@link GlitterProps} for available props
|
|
687
|
+
* @see {@link GlitterRef} for ref methods
|
|
688
|
+
*/
|
|
689
|
+
export const Glitter = memo(ForwardedGlitter);
|
|
690
|
+
|
|
691
|
+
// Set display name for React DevTools
|
|
692
|
+
Glitter.displayName = 'Glitter';
|
|
408
693
|
|
|
409
694
|
export default Glitter;
|