react-native-puff-pop 1.0.1 → 1.0.3
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/LICENSE +1 -0
- package/README.md +21 -0
- package/lib/module/index.js +124 -44
- package/lib/typescript/src/index.d.ts +13 -1
- package/package.json +1 -1
- package/src/index.tsx +153 -47
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -133,6 +133,25 @@ function App() {
|
|
|
133
133
|
}
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
+
### Loop Animation
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
// Loop infinitely
|
|
140
|
+
<PuffPop effect="rotate" loop={true}>
|
|
141
|
+
<LoadingSpinner />
|
|
142
|
+
</PuffPop>
|
|
143
|
+
|
|
144
|
+
// Loop 3 times
|
|
145
|
+
<PuffPop effect="bounce" loop={3}>
|
|
146
|
+
<NotificationBadge />
|
|
147
|
+
</PuffPop>
|
|
148
|
+
|
|
149
|
+
// Loop with delay between iterations
|
|
150
|
+
<PuffPop effect="scale" loop={true} loopDelay={500}>
|
|
151
|
+
<PulsingDot />
|
|
152
|
+
</PuffPop>
|
|
153
|
+
```
|
|
154
|
+
|
|
136
155
|
## Props
|
|
137
156
|
|
|
138
157
|
| Prop | Type | Default | Description |
|
|
@@ -147,6 +166,8 @@ function App() {
|
|
|
147
166
|
| `animateOnMount` | `boolean` | `true` | Animate when component mounts |
|
|
148
167
|
| `onAnimationComplete` | `() => void` | - | Callback when animation completes |
|
|
149
168
|
| `style` | `ViewStyle` | - | Custom container style |
|
|
169
|
+
| `loop` | `boolean \| number` | `false` | Loop animation (true=infinite, number=times) |
|
|
170
|
+
| `loopDelay` | `number` | `0` | Delay between loop iterations in ms |
|
|
150
171
|
|
|
151
172
|
### Animation Effects (`PuffPopEffect`)
|
|
152
173
|
|
package/lib/module/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useRef, useState, useCallback, } from 'react';
|
|
2
|
+
import { useEffect, useRef, useState, useCallback, useMemo, } from 'react';
|
|
3
3
|
import { View, Animated, StyleSheet, Easing, } from 'react-native';
|
|
4
4
|
/**
|
|
5
5
|
* Get easing function based on type
|
|
@@ -25,7 +25,7 @@ function getEasing(type) {
|
|
|
25
25
|
/**
|
|
26
26
|
* PuffPop - Animate children with beautiful entrance effects
|
|
27
27
|
*/
|
|
28
|
-
export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0, easing = 'easeOut', skeleton = true, visible = true, onAnimationComplete, style, animateOnMount = true, }) {
|
|
28
|
+
export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0, easing = 'easeOut', skeleton = true, visible = true, onAnimationComplete, style, animateOnMount = true, loop = false, loopDelay = 0, }) {
|
|
29
29
|
// Animation values
|
|
30
30
|
const opacity = useRef(new Animated.Value(animateOnMount ? 0 : 1)).current;
|
|
31
31
|
const scale = useRef(new Animated.Value(animateOnMount ? getInitialScale(effect) : 1)).current;
|
|
@@ -36,6 +36,26 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
|
|
|
36
36
|
const [measuredHeight, setMeasuredHeight] = useState(null);
|
|
37
37
|
const animatedHeight = useRef(new Animated.Value(0)).current;
|
|
38
38
|
const hasAnimated = useRef(false);
|
|
39
|
+
const loopAnimationRef = useRef(null);
|
|
40
|
+
const loopTimeoutRef = useRef(null);
|
|
41
|
+
// Memoize effect type checks to avoid repeated includes() calls
|
|
42
|
+
const effectFlags = useMemo(() => ({
|
|
43
|
+
hasScale: ['scale', 'bounce', 'zoom', 'rotateScale', 'flip'].includes(effect),
|
|
44
|
+
hasRotate: ['rotate', 'rotateScale'].includes(effect),
|
|
45
|
+
hasFlip: effect === 'flip',
|
|
46
|
+
hasTranslateX: ['slideLeft', 'slideRight'].includes(effect),
|
|
47
|
+
hasTranslateY: ['slideUp', 'slideDown', 'bounce'].includes(effect),
|
|
48
|
+
hasRotateEffect: ['rotate', 'rotateScale', 'flip'].includes(effect),
|
|
49
|
+
}), [effect]);
|
|
50
|
+
// Memoize interpolations to avoid recreating on every render
|
|
51
|
+
const rotateInterpolation = useMemo(() => rotate.interpolate({
|
|
52
|
+
inputRange: [-360, 0, 360],
|
|
53
|
+
outputRange: ['-360deg', '0deg', '360deg'],
|
|
54
|
+
}), [rotate]);
|
|
55
|
+
const flipInterpolation = useMemo(() => rotate.interpolate({
|
|
56
|
+
inputRange: [-180, 0],
|
|
57
|
+
outputRange: ['-180deg', '0deg'],
|
|
58
|
+
}), [rotate]);
|
|
39
59
|
// Handle layout measurement for non-skeleton mode
|
|
40
60
|
const onLayout = useCallback((event) => {
|
|
41
61
|
if (!skeleton && measuredHeight === null) {
|
|
@@ -61,7 +81,7 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
|
|
|
61
81
|
...config,
|
|
62
82
|
}));
|
|
63
83
|
// Scale animation
|
|
64
|
-
if (
|
|
84
|
+
if (effectFlags.hasScale) {
|
|
65
85
|
const targetScale = toVisible ? 1 : getInitialScale(effect);
|
|
66
86
|
animations.push(Animated.timing(scale, {
|
|
67
87
|
toValue: targetScale,
|
|
@@ -70,7 +90,7 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
|
|
|
70
90
|
}));
|
|
71
91
|
}
|
|
72
92
|
// Rotate animation
|
|
73
|
-
if (
|
|
93
|
+
if (effectFlags.hasRotate || effectFlags.hasFlip) {
|
|
74
94
|
const targetRotate = toVisible ? 0 : getInitialRotate(effect);
|
|
75
95
|
animations.push(Animated.timing(rotate, {
|
|
76
96
|
toValue: targetRotate,
|
|
@@ -78,7 +98,7 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
|
|
|
78
98
|
}));
|
|
79
99
|
}
|
|
80
100
|
// TranslateX animation
|
|
81
|
-
if (
|
|
101
|
+
if (effectFlags.hasTranslateX) {
|
|
82
102
|
const targetX = toVisible ? 0 : getInitialTranslateX(effect);
|
|
83
103
|
animations.push(Animated.timing(translateX, {
|
|
84
104
|
toValue: targetX,
|
|
@@ -86,7 +106,7 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
|
|
|
86
106
|
}));
|
|
87
107
|
}
|
|
88
108
|
// TranslateY animation
|
|
89
|
-
if (
|
|
109
|
+
if (effectFlags.hasTranslateY) {
|
|
90
110
|
const targetY = toVisible ? 0 : getInitialTranslateY(effect);
|
|
91
111
|
animations.push(Animated.timing(translateY, {
|
|
92
112
|
toValue: targetY,
|
|
@@ -105,18 +125,70 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
|
|
|
105
125
|
}
|
|
106
126
|
// Run animations with delay
|
|
107
127
|
const parallelAnimation = Animated.parallel(animations);
|
|
128
|
+
// Reset values function for looping
|
|
129
|
+
const resetValues = () => {
|
|
130
|
+
opacity.setValue(0);
|
|
131
|
+
scale.setValue(getInitialScale(effect));
|
|
132
|
+
rotate.setValue(getInitialRotate(effect));
|
|
133
|
+
translateX.setValue(getInitialTranslateX(effect));
|
|
134
|
+
translateY.setValue(getInitialTranslateY(effect));
|
|
135
|
+
if (!skeleton && measuredHeight !== null) {
|
|
136
|
+
animatedHeight.setValue(0);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
// Build the animation sequence
|
|
140
|
+
let animation;
|
|
108
141
|
if (delay > 0) {
|
|
109
|
-
Animated.sequence([
|
|
142
|
+
animation = Animated.sequence([
|
|
110
143
|
Animated.delay(delay),
|
|
111
144
|
parallelAnimation,
|
|
112
|
-
])
|
|
113
|
-
if (toVisible && onAnimationComplete) {
|
|
114
|
-
onAnimationComplete();
|
|
115
|
-
}
|
|
116
|
-
});
|
|
145
|
+
]);
|
|
117
146
|
}
|
|
118
147
|
else {
|
|
119
|
-
|
|
148
|
+
animation = parallelAnimation;
|
|
149
|
+
}
|
|
150
|
+
// Handle loop option
|
|
151
|
+
if (toVisible && loop) {
|
|
152
|
+
// Stop any existing loop animation
|
|
153
|
+
if (loopAnimationRef.current) {
|
|
154
|
+
loopAnimationRef.current.stop();
|
|
155
|
+
}
|
|
156
|
+
const loopCount = typeof loop === 'number' ? loop : -1;
|
|
157
|
+
let currentIteration = 0;
|
|
158
|
+
const runLoop = () => {
|
|
159
|
+
resetValues();
|
|
160
|
+
animation.start(({ finished }) => {
|
|
161
|
+
if (finished) {
|
|
162
|
+
currentIteration++;
|
|
163
|
+
if (loopCount === -1 || currentIteration < loopCount) {
|
|
164
|
+
// Add delay between loops if specified
|
|
165
|
+
if (loopDelay > 0) {
|
|
166
|
+
// Clear any existing timeout before setting a new one
|
|
167
|
+
if (loopTimeoutRef.current) {
|
|
168
|
+
clearTimeout(loopTimeoutRef.current);
|
|
169
|
+
}
|
|
170
|
+
loopTimeoutRef.current = setTimeout(runLoop, loopDelay);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
runLoop();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else if (onAnimationComplete) {
|
|
177
|
+
onAnimationComplete();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
// Store reference and start
|
|
183
|
+
runLoop();
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
// Stop any existing loop animation
|
|
187
|
+
if (loopAnimationRef.current) {
|
|
188
|
+
loopAnimationRef.current.stop();
|
|
189
|
+
loopAnimationRef.current = null;
|
|
190
|
+
}
|
|
191
|
+
animation.start(() => {
|
|
120
192
|
if (toVisible && onAnimationComplete) {
|
|
121
193
|
onAnimationComplete();
|
|
122
194
|
}
|
|
@@ -127,6 +199,7 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
|
|
|
127
199
|
duration,
|
|
128
200
|
easing,
|
|
129
201
|
effect,
|
|
202
|
+
effectFlags,
|
|
130
203
|
measuredHeight,
|
|
131
204
|
onAnimationComplete,
|
|
132
205
|
opacity,
|
|
@@ -136,6 +209,8 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
|
|
|
136
209
|
translateX,
|
|
137
210
|
translateY,
|
|
138
211
|
animatedHeight,
|
|
212
|
+
loop,
|
|
213
|
+
loopDelay,
|
|
139
214
|
]);
|
|
140
215
|
// Handle initial mount animation
|
|
141
216
|
useEffect(() => {
|
|
@@ -150,36 +225,30 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
|
|
|
150
225
|
animate(visible);
|
|
151
226
|
}
|
|
152
227
|
}, [visible, animate]);
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
return (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
228
|
+
// Cleanup loop animation and timeout on unmount
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
return () => {
|
|
231
|
+
if (loopAnimationRef.current) {
|
|
232
|
+
loopAnimationRef.current.stop();
|
|
233
|
+
}
|
|
234
|
+
if (loopTimeoutRef.current) {
|
|
235
|
+
clearTimeout(loopTimeoutRef.current);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}, []);
|
|
239
|
+
// Memoize transform array to avoid recreating on every render
|
|
240
|
+
// IMPORTANT: All hooks must be called before any conditional returns
|
|
241
|
+
const transform = useMemo(() => {
|
|
242
|
+
const { hasScale, hasRotate, hasFlip, hasTranslateX, hasTranslateY } = effectFlags;
|
|
164
243
|
const transforms = [];
|
|
165
244
|
if (hasScale) {
|
|
166
245
|
transforms.push({ scale });
|
|
167
246
|
}
|
|
168
247
|
if (hasRotate) {
|
|
169
|
-
transforms.push({
|
|
170
|
-
rotate: rotate.interpolate({
|
|
171
|
-
inputRange: [-360, 0, 360],
|
|
172
|
-
outputRange: ['-360deg', '0deg', '360deg'],
|
|
173
|
-
}),
|
|
174
|
-
});
|
|
248
|
+
transforms.push({ rotate: rotateInterpolation });
|
|
175
249
|
}
|
|
176
250
|
if (hasFlip) {
|
|
177
|
-
transforms.push({
|
|
178
|
-
rotateY: rotate.interpolate({
|
|
179
|
-
inputRange: [-180, 0],
|
|
180
|
-
outputRange: ['-180deg', '0deg'],
|
|
181
|
-
}),
|
|
182
|
-
});
|
|
251
|
+
transforms.push({ rotateY: flipInterpolation });
|
|
183
252
|
}
|
|
184
253
|
if (hasTranslateX) {
|
|
185
254
|
transforms.push({ translateX });
|
|
@@ -188,15 +257,26 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
|
|
|
188
257
|
transforms.push({ translateY });
|
|
189
258
|
}
|
|
190
259
|
return transforms.length > 0 ? transforms : undefined;
|
|
191
|
-
};
|
|
192
|
-
|
|
260
|
+
}, [effectFlags, scale, rotateInterpolation, flipInterpolation, translateX, translateY]);
|
|
261
|
+
// Memoize animated style
|
|
262
|
+
const animatedStyle = useMemo(() => ({
|
|
193
263
|
opacity,
|
|
194
|
-
transform
|
|
195
|
-
};
|
|
196
|
-
//
|
|
197
|
-
const containerAnimatedStyle =
|
|
198
|
-
|
|
199
|
-
|
|
264
|
+
transform,
|
|
265
|
+
}), [opacity, transform]);
|
|
266
|
+
// Memoize container style for non-skeleton mode
|
|
267
|
+
const containerAnimatedStyle = useMemo(() => {
|
|
268
|
+
if (!skeleton && measuredHeight !== null) {
|
|
269
|
+
return {
|
|
270
|
+
height: animatedHeight,
|
|
271
|
+
overflow: effectFlags.hasRotateEffect ? 'visible' : 'hidden'
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
return {};
|
|
275
|
+
}, [skeleton, measuredHeight, animatedHeight, effectFlags.hasRotateEffect]);
|
|
276
|
+
// For non-skeleton mode, measure first (after all hooks)
|
|
277
|
+
if (!skeleton && measuredHeight === null) {
|
|
278
|
+
return (_jsx(View, { style: styles.measureContainer, onLayout: onLayout, children: _jsx(View, { style: styles.hidden, children: children }) }));
|
|
279
|
+
}
|
|
200
280
|
return (_jsx(Animated.View, { style: [styles.container, style, containerAnimatedStyle], children: _jsx(Animated.View, { style: animatedStyle, children: children }) }));
|
|
201
281
|
}
|
|
202
282
|
/**
|
|
@@ -57,9 +57,21 @@ export interface PuffPopProps {
|
|
|
57
57
|
* @default true
|
|
58
58
|
*/
|
|
59
59
|
animateOnMount?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Loop the animation
|
|
62
|
+
* - true: loop infinitely
|
|
63
|
+
* - number: loop specific number of times
|
|
64
|
+
* @default false
|
|
65
|
+
*/
|
|
66
|
+
loop?: boolean | number;
|
|
67
|
+
/**
|
|
68
|
+
* Delay between loop iterations in milliseconds
|
|
69
|
+
* @default 0
|
|
70
|
+
*/
|
|
71
|
+
loopDelay?: number;
|
|
60
72
|
}
|
|
61
73
|
/**
|
|
62
74
|
* PuffPop - Animate children with beautiful entrance effects
|
|
63
75
|
*/
|
|
64
|
-
export declare function PuffPop({ children, effect, duration, delay, easing, skeleton, visible, onAnimationComplete, style, animateOnMount, }: PuffPopProps): ReactElement;
|
|
76
|
+
export declare function PuffPop({ children, effect, duration, delay, easing, skeleton, visible, onAnimationComplete, style, animateOnMount, loop, loopDelay, }: PuffPopProps): ReactElement;
|
|
65
77
|
export default PuffPop;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-puff-pop",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "A React Native animation library for revealing children components with beautiful puff and pop effects",
|
|
5
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"types": "./lib/typescript/src/index.d.ts",
|
package/src/index.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
useRef,
|
|
4
4
|
useState,
|
|
5
5
|
useCallback,
|
|
6
|
+
useMemo,
|
|
6
7
|
type ReactNode,
|
|
7
8
|
type ReactElement,
|
|
8
9
|
} from 'react';
|
|
@@ -101,6 +102,20 @@ export interface PuffPopProps {
|
|
|
101
102
|
* @default true
|
|
102
103
|
*/
|
|
103
104
|
animateOnMount?: boolean;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Loop the animation
|
|
108
|
+
* - true: loop infinitely
|
|
109
|
+
* - number: loop specific number of times
|
|
110
|
+
* @default false
|
|
111
|
+
*/
|
|
112
|
+
loop?: boolean | number;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Delay between loop iterations in milliseconds
|
|
116
|
+
* @default 0
|
|
117
|
+
*/
|
|
118
|
+
loopDelay?: number;
|
|
104
119
|
}
|
|
105
120
|
|
|
106
121
|
/**
|
|
@@ -139,6 +154,8 @@ export function PuffPop({
|
|
|
139
154
|
onAnimationComplete,
|
|
140
155
|
style,
|
|
141
156
|
animateOnMount = true,
|
|
157
|
+
loop = false,
|
|
158
|
+
loopDelay = 0,
|
|
142
159
|
}: PuffPopProps): ReactElement {
|
|
143
160
|
// Animation values
|
|
144
161
|
const opacity = useRef(new Animated.Value(animateOnMount ? 0 : 1)).current;
|
|
@@ -151,6 +168,31 @@ export function PuffPop({
|
|
|
151
168
|
const [measuredHeight, setMeasuredHeight] = useState<number | null>(null);
|
|
152
169
|
const animatedHeight = useRef(new Animated.Value(0)).current;
|
|
153
170
|
const hasAnimated = useRef(false);
|
|
171
|
+
const loopAnimationRef = useRef<Animated.CompositeAnimation | null>(null);
|
|
172
|
+
const loopTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
173
|
+
|
|
174
|
+
// Memoize effect type checks to avoid repeated includes() calls
|
|
175
|
+
const effectFlags = useMemo(() => ({
|
|
176
|
+
hasScale: ['scale', 'bounce', 'zoom', 'rotateScale', 'flip'].includes(effect),
|
|
177
|
+
hasRotate: ['rotate', 'rotateScale'].includes(effect),
|
|
178
|
+
hasFlip: effect === 'flip',
|
|
179
|
+
hasTranslateX: ['slideLeft', 'slideRight'].includes(effect),
|
|
180
|
+
hasTranslateY: ['slideUp', 'slideDown', 'bounce'].includes(effect),
|
|
181
|
+
hasRotateEffect: ['rotate', 'rotateScale', 'flip'].includes(effect),
|
|
182
|
+
}), [effect]);
|
|
183
|
+
|
|
184
|
+
// Memoize interpolations to avoid recreating on every render
|
|
185
|
+
const rotateInterpolation = useMemo(() =>
|
|
186
|
+
rotate.interpolate({
|
|
187
|
+
inputRange: [-360, 0, 360],
|
|
188
|
+
outputRange: ['-360deg', '0deg', '360deg'],
|
|
189
|
+
}), [rotate]);
|
|
190
|
+
|
|
191
|
+
const flipInterpolation = useMemo(() =>
|
|
192
|
+
rotate.interpolate({
|
|
193
|
+
inputRange: [-180, 0],
|
|
194
|
+
outputRange: ['-180deg', '0deg'],
|
|
195
|
+
}), [rotate]);
|
|
154
196
|
|
|
155
197
|
// Handle layout measurement for non-skeleton mode
|
|
156
198
|
const onLayout = useCallback(
|
|
@@ -187,7 +229,7 @@ export function PuffPop({
|
|
|
187
229
|
);
|
|
188
230
|
|
|
189
231
|
// Scale animation
|
|
190
|
-
if (
|
|
232
|
+
if (effectFlags.hasScale) {
|
|
191
233
|
const targetScale = toVisible ? 1 : getInitialScale(effect);
|
|
192
234
|
animations.push(
|
|
193
235
|
Animated.timing(scale, {
|
|
@@ -199,7 +241,7 @@ export function PuffPop({
|
|
|
199
241
|
}
|
|
200
242
|
|
|
201
243
|
// Rotate animation
|
|
202
|
-
if (
|
|
244
|
+
if (effectFlags.hasRotate || effectFlags.hasFlip) {
|
|
203
245
|
const targetRotate = toVisible ? 0 : getInitialRotate(effect);
|
|
204
246
|
animations.push(
|
|
205
247
|
Animated.timing(rotate, {
|
|
@@ -210,7 +252,7 @@ export function PuffPop({
|
|
|
210
252
|
}
|
|
211
253
|
|
|
212
254
|
// TranslateX animation
|
|
213
|
-
if (
|
|
255
|
+
if (effectFlags.hasTranslateX) {
|
|
214
256
|
const targetX = toVisible ? 0 : getInitialTranslateX(effect);
|
|
215
257
|
animations.push(
|
|
216
258
|
Animated.timing(translateX, {
|
|
@@ -221,7 +263,7 @@ export function PuffPop({
|
|
|
221
263
|
}
|
|
222
264
|
|
|
223
265
|
// TranslateY animation
|
|
224
|
-
if (
|
|
266
|
+
if (effectFlags.hasTranslateY) {
|
|
225
267
|
const targetY = toVisible ? 0 : getInitialTranslateY(effect);
|
|
226
268
|
animations.push(
|
|
227
269
|
Animated.timing(translateY, {
|
|
@@ -246,18 +288,74 @@ export function PuffPop({
|
|
|
246
288
|
|
|
247
289
|
// Run animations with delay
|
|
248
290
|
const parallelAnimation = Animated.parallel(animations);
|
|
291
|
+
|
|
292
|
+
// Reset values function for looping
|
|
293
|
+
const resetValues = () => {
|
|
294
|
+
opacity.setValue(0);
|
|
295
|
+
scale.setValue(getInitialScale(effect));
|
|
296
|
+
rotate.setValue(getInitialRotate(effect));
|
|
297
|
+
translateX.setValue(getInitialTranslateX(effect));
|
|
298
|
+
translateY.setValue(getInitialTranslateY(effect));
|
|
299
|
+
if (!skeleton && measuredHeight !== null) {
|
|
300
|
+
animatedHeight.setValue(0);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
// Build the animation sequence
|
|
305
|
+
let animation: Animated.CompositeAnimation;
|
|
249
306
|
|
|
250
307
|
if (delay > 0) {
|
|
251
|
-
Animated.sequence([
|
|
308
|
+
animation = Animated.sequence([
|
|
252
309
|
Animated.delay(delay),
|
|
253
310
|
parallelAnimation,
|
|
254
|
-
])
|
|
255
|
-
if (toVisible && onAnimationComplete) {
|
|
256
|
-
onAnimationComplete();
|
|
257
|
-
}
|
|
258
|
-
});
|
|
311
|
+
]);
|
|
259
312
|
} else {
|
|
260
|
-
|
|
313
|
+
animation = parallelAnimation;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Handle loop option
|
|
317
|
+
if (toVisible && loop) {
|
|
318
|
+
// Stop any existing loop animation
|
|
319
|
+
if (loopAnimationRef.current) {
|
|
320
|
+
loopAnimationRef.current.stop();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const loopCount = typeof loop === 'number' ? loop : -1;
|
|
324
|
+
let currentIteration = 0;
|
|
325
|
+
|
|
326
|
+
const runLoop = () => {
|
|
327
|
+
resetValues();
|
|
328
|
+
animation.start(({ finished }) => {
|
|
329
|
+
if (finished) {
|
|
330
|
+
currentIteration++;
|
|
331
|
+
if (loopCount === -1 || currentIteration < loopCount) {
|
|
332
|
+
// Add delay between loops if specified
|
|
333
|
+
if (loopDelay > 0) {
|
|
334
|
+
// Clear any existing timeout before setting a new one
|
|
335
|
+
if (loopTimeoutRef.current) {
|
|
336
|
+
clearTimeout(loopTimeoutRef.current);
|
|
337
|
+
}
|
|
338
|
+
loopTimeoutRef.current = setTimeout(runLoop, loopDelay);
|
|
339
|
+
} else {
|
|
340
|
+
runLoop();
|
|
341
|
+
}
|
|
342
|
+
} else if (onAnimationComplete) {
|
|
343
|
+
onAnimationComplete();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
// Store reference and start
|
|
350
|
+
runLoop();
|
|
351
|
+
} else {
|
|
352
|
+
// Stop any existing loop animation
|
|
353
|
+
if (loopAnimationRef.current) {
|
|
354
|
+
loopAnimationRef.current.stop();
|
|
355
|
+
loopAnimationRef.current = null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
animation.start(() => {
|
|
261
359
|
if (toVisible && onAnimationComplete) {
|
|
262
360
|
onAnimationComplete();
|
|
263
361
|
}
|
|
@@ -269,6 +367,7 @@ export function PuffPop({
|
|
|
269
367
|
duration,
|
|
270
368
|
easing,
|
|
271
369
|
effect,
|
|
370
|
+
effectFlags,
|
|
272
371
|
measuredHeight,
|
|
273
372
|
onAnimationComplete,
|
|
274
373
|
opacity,
|
|
@@ -278,6 +377,8 @@ export function PuffPop({
|
|
|
278
377
|
translateX,
|
|
279
378
|
translateY,
|
|
280
379
|
animatedHeight,
|
|
380
|
+
loop,
|
|
381
|
+
loopDelay,
|
|
281
382
|
]
|
|
282
383
|
);
|
|
283
384
|
|
|
@@ -296,23 +397,22 @@ export function PuffPop({
|
|
|
296
397
|
}
|
|
297
398
|
}, [visible, animate]);
|
|
298
399
|
|
|
299
|
-
//
|
|
300
|
-
|
|
301
|
-
return (
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const hasScale = ['scale', 'bounce', 'zoom', 'rotateScale', 'flip'].includes(effect);
|
|
311
|
-
const hasRotate = ['rotate', 'rotateScale'].includes(effect);
|
|
312
|
-
const hasFlip = effect === 'flip';
|
|
313
|
-
const hasTranslateX = ['slideLeft', 'slideRight'].includes(effect);
|
|
314
|
-
const hasTranslateY = ['slideUp', 'slideDown', 'bounce'].includes(effect);
|
|
400
|
+
// Cleanup loop animation and timeout on unmount
|
|
401
|
+
useEffect(() => {
|
|
402
|
+
return () => {
|
|
403
|
+
if (loopAnimationRef.current) {
|
|
404
|
+
loopAnimationRef.current.stop();
|
|
405
|
+
}
|
|
406
|
+
if (loopTimeoutRef.current) {
|
|
407
|
+
clearTimeout(loopTimeoutRef.current);
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
}, []);
|
|
315
411
|
|
|
412
|
+
// Memoize transform array to avoid recreating on every render
|
|
413
|
+
// IMPORTANT: All hooks must be called before any conditional returns
|
|
414
|
+
const transform = useMemo(() => {
|
|
415
|
+
const { hasScale, hasRotate, hasFlip, hasTranslateX, hasTranslateY } = effectFlags;
|
|
316
416
|
const transforms = [];
|
|
317
417
|
|
|
318
418
|
if (hasScale) {
|
|
@@ -320,21 +420,11 @@ export function PuffPop({
|
|
|
320
420
|
}
|
|
321
421
|
|
|
322
422
|
if (hasRotate) {
|
|
323
|
-
transforms.push({
|
|
324
|
-
rotate: rotate.interpolate({
|
|
325
|
-
inputRange: [-360, 0, 360],
|
|
326
|
-
outputRange: ['-360deg', '0deg', '360deg'],
|
|
327
|
-
}),
|
|
328
|
-
});
|
|
423
|
+
transforms.push({ rotate: rotateInterpolation });
|
|
329
424
|
}
|
|
330
425
|
|
|
331
426
|
if (hasFlip) {
|
|
332
|
-
transforms.push({
|
|
333
|
-
rotateY: rotate.interpolate({
|
|
334
|
-
inputRange: [-180, 0],
|
|
335
|
-
outputRange: ['-180deg', '0deg'],
|
|
336
|
-
}),
|
|
337
|
-
});
|
|
427
|
+
transforms.push({ rotateY: flipInterpolation });
|
|
338
428
|
}
|
|
339
429
|
|
|
340
430
|
if (hasTranslateX) {
|
|
@@ -346,17 +436,33 @@ export function PuffPop({
|
|
|
346
436
|
}
|
|
347
437
|
|
|
348
438
|
return transforms.length > 0 ? transforms : undefined;
|
|
349
|
-
};
|
|
439
|
+
}, [effectFlags, scale, rotateInterpolation, flipInterpolation, translateX, translateY]);
|
|
350
440
|
|
|
351
|
-
|
|
441
|
+
// Memoize animated style
|
|
442
|
+
const animatedStyle = useMemo(() => ({
|
|
352
443
|
opacity,
|
|
353
|
-
transform
|
|
354
|
-
};
|
|
444
|
+
transform,
|
|
445
|
+
}), [opacity, transform]);
|
|
446
|
+
|
|
447
|
+
// Memoize container style for non-skeleton mode
|
|
448
|
+
const containerAnimatedStyle = useMemo(() => {
|
|
449
|
+
if (!skeleton && measuredHeight !== null) {
|
|
450
|
+
return {
|
|
451
|
+
height: animatedHeight,
|
|
452
|
+
overflow: effectFlags.hasRotateEffect ? 'visible' as const : 'hidden' as const
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
return {};
|
|
456
|
+
}, [skeleton, measuredHeight, animatedHeight, effectFlags.hasRotateEffect]);
|
|
355
457
|
|
|
356
|
-
//
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
458
|
+
// For non-skeleton mode, measure first (after all hooks)
|
|
459
|
+
if (!skeleton && measuredHeight === null) {
|
|
460
|
+
return (
|
|
461
|
+
<View style={styles.measureContainer} onLayout={onLayout}>
|
|
462
|
+
<View style={styles.hidden}>{children}</View>
|
|
463
|
+
</View>
|
|
464
|
+
);
|
|
465
|
+
}
|
|
360
466
|
|
|
361
467
|
return (
|
|
362
468
|
<Animated.View style={[styles.container, style, containerAnimatedStyle]}>
|