react-native-puff-pop 1.0.1
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 +22 -0
- package/README.md +192 -0
- package/lib/module/index.js +274 -0
- package/lib/typescript/src/index.d.ts +65 -0
- package/package.json +89 -0
- package/src/index.tsx +446 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 liveforownhappiness
|
|
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.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# react-native-puff-pop 🎉
|
|
2
|
+
|
|
3
|
+
A React Native animation library for revealing children components with beautiful puff and pop effects.
|
|
4
|
+
|
|
5
|
+
Works with both **React Native CLI** and **Expo** projects - no native dependencies required!
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
## Demo
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<img src="./assets/demo.gif" alt="React Native PuffPop Demo" width="320" />
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- 🎬 **11 Animation Effects**: scale, rotate, fade, slideUp, slideDown, slideLeft, slideRight, bounce, flip, zoom, rotateScale
|
|
19
|
+
- 🦴 **Skeleton Mode**: Reserve space before animation or expand from zero height
|
|
20
|
+
- ⚡ **Native Driver Support**: Smooth 60fps animations
|
|
21
|
+
- 🎯 **Easy to Use**: Just wrap your components with `<PuffPop>`
|
|
22
|
+
- 📱 **Cross Platform**: Works on iOS, Android, and Web
|
|
23
|
+
- 🔧 **TypeScript**: Full TypeScript support with type definitions
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Using npm
|
|
29
|
+
npm install react-native-puff-pop
|
|
30
|
+
|
|
31
|
+
# Using yarn
|
|
32
|
+
yarn add react-native-puff-pop
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
### Basic Usage
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import { PuffPop } from 'react-native-puff-pop';
|
|
41
|
+
|
|
42
|
+
function App() {
|
|
43
|
+
return (
|
|
44
|
+
<PuffPop>
|
|
45
|
+
<View style={styles.card}>
|
|
46
|
+
<Text>Hello, PuffPop!</Text>
|
|
47
|
+
</View>
|
|
48
|
+
</PuffPop>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### With Different Effects
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
// Scale from center (default)
|
|
57
|
+
<PuffPop effect="scale">
|
|
58
|
+
<YourComponent />
|
|
59
|
+
</PuffPop>
|
|
60
|
+
|
|
61
|
+
// Rotate while appearing
|
|
62
|
+
<PuffPop effect="rotate">
|
|
63
|
+
<YourComponent />
|
|
64
|
+
</PuffPop>
|
|
65
|
+
|
|
66
|
+
// Rotate + Scale combined
|
|
67
|
+
<PuffPop effect="rotateScale" easing="spring">
|
|
68
|
+
<YourComponent />
|
|
69
|
+
</PuffPop>
|
|
70
|
+
|
|
71
|
+
// Bounce effect
|
|
72
|
+
<PuffPop effect="bounce" duration={600}>
|
|
73
|
+
<YourComponent />
|
|
74
|
+
</PuffPop>
|
|
75
|
+
|
|
76
|
+
// Slide from bottom
|
|
77
|
+
<PuffPop effect="slideUp">
|
|
78
|
+
<YourComponent />
|
|
79
|
+
</PuffPop>
|
|
80
|
+
|
|
81
|
+
// 3D Flip effect
|
|
82
|
+
<PuffPop effect="flip">
|
|
83
|
+
<YourComponent />
|
|
84
|
+
</PuffPop>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Skeleton Mode
|
|
88
|
+
|
|
89
|
+
By default, `skeleton={true}` reserves space for the component before animation:
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
// Reserves space (default)
|
|
93
|
+
<PuffPop skeleton={true}>
|
|
94
|
+
<YourComponent />
|
|
95
|
+
</PuffPop>
|
|
96
|
+
|
|
97
|
+
// Expands from zero height, pushing content below
|
|
98
|
+
<PuffPop skeleton={false}>
|
|
99
|
+
<YourComponent />
|
|
100
|
+
</PuffPop>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Staggered Animations
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
<View>
|
|
107
|
+
<PuffPop delay={0}>
|
|
108
|
+
<Card title="First" />
|
|
109
|
+
</PuffPop>
|
|
110
|
+
<PuffPop delay={100}>
|
|
111
|
+
<Card title="Second" />
|
|
112
|
+
</PuffPop>
|
|
113
|
+
<PuffPop delay={200}>
|
|
114
|
+
<Card title="Third" />
|
|
115
|
+
</PuffPop>
|
|
116
|
+
</View>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Controlled Visibility
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
function App() {
|
|
123
|
+
const [visible, setVisible] = useState(false);
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<>
|
|
127
|
+
<Button title="Toggle" onPress={() => setVisible(!visible)} />
|
|
128
|
+
<PuffPop visible={visible} animateOnMount={false}>
|
|
129
|
+
<YourComponent />
|
|
130
|
+
</PuffPop>
|
|
131
|
+
</>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Props
|
|
137
|
+
|
|
138
|
+
| Prop | Type | Default | Description |
|
|
139
|
+
|------|------|---------|-------------|
|
|
140
|
+
| `children` | `ReactNode` | - | Children to animate |
|
|
141
|
+
| `effect` | `PuffPopEffect` | `'scale'` | Animation effect type |
|
|
142
|
+
| `duration` | `number` | `400` | Animation duration in ms |
|
|
143
|
+
| `delay` | `number` | `0` | Delay before animation starts in ms |
|
|
144
|
+
| `easing` | `PuffPopEasing` | `'easeOut'` | Easing function |
|
|
145
|
+
| `skeleton` | `boolean` | `true` | Reserve space before animation |
|
|
146
|
+
| `visible` | `boolean` | `true` | Control visibility |
|
|
147
|
+
| `animateOnMount` | `boolean` | `true` | Animate when component mounts |
|
|
148
|
+
| `onAnimationComplete` | `() => void` | - | Callback when animation completes |
|
|
149
|
+
| `style` | `ViewStyle` | - | Custom container style |
|
|
150
|
+
|
|
151
|
+
### Animation Effects (`PuffPopEffect`)
|
|
152
|
+
|
|
153
|
+
| Effect | Description |
|
|
154
|
+
|--------|-------------|
|
|
155
|
+
| `scale` | Scale from center point |
|
|
156
|
+
| `rotate` | Full rotation (360°) while appearing |
|
|
157
|
+
| `fade` | Simple fade in |
|
|
158
|
+
| `slideUp` | Slide from bottom |
|
|
159
|
+
| `slideDown` | Slide from top |
|
|
160
|
+
| `slideLeft` | Slide from right |
|
|
161
|
+
| `slideRight` | Slide from left |
|
|
162
|
+
| `bounce` | Bounce effect with overshoot |
|
|
163
|
+
| `flip` | 3D flip effect |
|
|
164
|
+
| `zoom` | Zoom with slight overshoot |
|
|
165
|
+
| `rotateScale` | Rotate + Scale combined |
|
|
166
|
+
|
|
167
|
+
### Easing Types (`PuffPopEasing`)
|
|
168
|
+
|
|
169
|
+
| Easing | Description |
|
|
170
|
+
|--------|-------------|
|
|
171
|
+
| `linear` | Linear animation |
|
|
172
|
+
| `easeIn` | Slow start |
|
|
173
|
+
| `easeOut` | Slow end |
|
|
174
|
+
| `easeInOut` | Slow start and end |
|
|
175
|
+
| `spring` | Spring-like effect |
|
|
176
|
+
| `bounce` | Bouncing effect |
|
|
177
|
+
|
|
178
|
+
## Skeleton Mode Explained
|
|
179
|
+
|
|
180
|
+
### `skeleton={true}` (default)
|
|
181
|
+
The component reserves its full space immediately, and only the visual appearance animates. This is useful when you don't want layout shifts.
|
|
182
|
+
|
|
183
|
+
### `skeleton={false}`
|
|
184
|
+
The component's height starts at 0 and expands during animation, pushing other content below it. This creates a more dynamic entrance effect.
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
MIT
|
|
189
|
+
|
|
190
|
+
## Contributing
|
|
191
|
+
|
|
192
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState, useCallback, } from 'react';
|
|
3
|
+
import { View, Animated, StyleSheet, Easing, } from 'react-native';
|
|
4
|
+
/**
|
|
5
|
+
* Get easing function based on type
|
|
6
|
+
*/
|
|
7
|
+
function getEasing(type) {
|
|
8
|
+
switch (type) {
|
|
9
|
+
case 'linear':
|
|
10
|
+
return Easing.linear;
|
|
11
|
+
case 'easeIn':
|
|
12
|
+
return Easing.in(Easing.ease);
|
|
13
|
+
case 'easeOut':
|
|
14
|
+
return Easing.out(Easing.ease);
|
|
15
|
+
case 'easeInOut':
|
|
16
|
+
return Easing.inOut(Easing.ease);
|
|
17
|
+
case 'spring':
|
|
18
|
+
return Easing.out(Easing.back(1.5));
|
|
19
|
+
case 'bounce':
|
|
20
|
+
return Easing.bounce;
|
|
21
|
+
default:
|
|
22
|
+
return Easing.out(Easing.ease);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* PuffPop - Animate children with beautiful entrance effects
|
|
27
|
+
*/
|
|
28
|
+
export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0, easing = 'easeOut', skeleton = true, visible = true, onAnimationComplete, style, animateOnMount = true, }) {
|
|
29
|
+
// Animation values
|
|
30
|
+
const opacity = useRef(new Animated.Value(animateOnMount ? 0 : 1)).current;
|
|
31
|
+
const scale = useRef(new Animated.Value(animateOnMount ? getInitialScale(effect) : 1)).current;
|
|
32
|
+
const rotate = useRef(new Animated.Value(animateOnMount ? getInitialRotate(effect) : 0)).current;
|
|
33
|
+
const translateX = useRef(new Animated.Value(animateOnMount ? getInitialTranslateX(effect) : 0)).current;
|
|
34
|
+
const translateY = useRef(new Animated.Value(animateOnMount ? getInitialTranslateY(effect) : 0)).current;
|
|
35
|
+
// For non-skeleton mode
|
|
36
|
+
const [measuredHeight, setMeasuredHeight] = useState(null);
|
|
37
|
+
const animatedHeight = useRef(new Animated.Value(0)).current;
|
|
38
|
+
const hasAnimated = useRef(false);
|
|
39
|
+
// Handle layout measurement for non-skeleton mode
|
|
40
|
+
const onLayout = useCallback((event) => {
|
|
41
|
+
if (!skeleton && measuredHeight === null) {
|
|
42
|
+
const { height } = event.nativeEvent.layout;
|
|
43
|
+
setMeasuredHeight(height);
|
|
44
|
+
}
|
|
45
|
+
}, [skeleton, measuredHeight]);
|
|
46
|
+
// Animate function
|
|
47
|
+
const animate = useCallback((toVisible) => {
|
|
48
|
+
const easingFn = getEasing(easing);
|
|
49
|
+
// When skeleton is false, we animate height which doesn't support native driver
|
|
50
|
+
// So we must use JS driver for all animations in that case
|
|
51
|
+
const useNative = skeleton;
|
|
52
|
+
const config = {
|
|
53
|
+
duration,
|
|
54
|
+
easing: easingFn,
|
|
55
|
+
useNativeDriver: useNative,
|
|
56
|
+
};
|
|
57
|
+
const animations = [];
|
|
58
|
+
// Opacity animation
|
|
59
|
+
animations.push(Animated.timing(opacity, {
|
|
60
|
+
toValue: toVisible ? 1 : 0,
|
|
61
|
+
...config,
|
|
62
|
+
}));
|
|
63
|
+
// Scale animation
|
|
64
|
+
if (['scale', 'bounce', 'zoom', 'rotateScale', 'flip'].includes(effect)) {
|
|
65
|
+
const targetScale = toVisible ? 1 : getInitialScale(effect);
|
|
66
|
+
animations.push(Animated.timing(scale, {
|
|
67
|
+
toValue: targetScale,
|
|
68
|
+
...config,
|
|
69
|
+
easing: effect === 'bounce' ? Easing.bounce : easingFn,
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
// Rotate animation
|
|
73
|
+
if (['rotate', 'rotateScale', 'flip'].includes(effect)) {
|
|
74
|
+
const targetRotate = toVisible ? 0 : getInitialRotate(effect);
|
|
75
|
+
animations.push(Animated.timing(rotate, {
|
|
76
|
+
toValue: targetRotate,
|
|
77
|
+
...config,
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
// TranslateX animation
|
|
81
|
+
if (['slideLeft', 'slideRight'].includes(effect)) {
|
|
82
|
+
const targetX = toVisible ? 0 : getInitialTranslateX(effect);
|
|
83
|
+
animations.push(Animated.timing(translateX, {
|
|
84
|
+
toValue: targetX,
|
|
85
|
+
...config,
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
// TranslateY animation
|
|
89
|
+
if (['slideUp', 'slideDown', 'bounce'].includes(effect)) {
|
|
90
|
+
const targetY = toVisible ? 0 : getInitialTranslateY(effect);
|
|
91
|
+
animations.push(Animated.timing(translateY, {
|
|
92
|
+
toValue: targetY,
|
|
93
|
+
...config,
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
// Height animation for non-skeleton mode
|
|
97
|
+
if (!skeleton && measuredHeight !== null) {
|
|
98
|
+
const targetHeight = toVisible ? measuredHeight : 0;
|
|
99
|
+
animations.push(Animated.timing(animatedHeight, {
|
|
100
|
+
toValue: targetHeight,
|
|
101
|
+
duration,
|
|
102
|
+
easing: easingFn,
|
|
103
|
+
useNativeDriver: false,
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
// Run animations with delay
|
|
107
|
+
const parallelAnimation = Animated.parallel(animations);
|
|
108
|
+
if (delay > 0) {
|
|
109
|
+
Animated.sequence([
|
|
110
|
+
Animated.delay(delay),
|
|
111
|
+
parallelAnimation,
|
|
112
|
+
]).start(() => {
|
|
113
|
+
if (toVisible && onAnimationComplete) {
|
|
114
|
+
onAnimationComplete();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
parallelAnimation.start(() => {
|
|
120
|
+
if (toVisible && onAnimationComplete) {
|
|
121
|
+
onAnimationComplete();
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}, [
|
|
126
|
+
delay,
|
|
127
|
+
duration,
|
|
128
|
+
easing,
|
|
129
|
+
effect,
|
|
130
|
+
measuredHeight,
|
|
131
|
+
onAnimationComplete,
|
|
132
|
+
opacity,
|
|
133
|
+
rotate,
|
|
134
|
+
scale,
|
|
135
|
+
skeleton,
|
|
136
|
+
translateX,
|
|
137
|
+
translateY,
|
|
138
|
+
animatedHeight,
|
|
139
|
+
]);
|
|
140
|
+
// Handle initial mount animation
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
if (animateOnMount && !hasAnimated.current && visible) {
|
|
143
|
+
hasAnimated.current = true;
|
|
144
|
+
animate(true);
|
|
145
|
+
}
|
|
146
|
+
}, [animate, animateOnMount, visible]);
|
|
147
|
+
// Handle visibility changes after mount
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
if (hasAnimated.current) {
|
|
150
|
+
animate(visible);
|
|
151
|
+
}
|
|
152
|
+
}, [visible, animate]);
|
|
153
|
+
// For non-skeleton mode, measure first
|
|
154
|
+
if (!skeleton && measuredHeight === null) {
|
|
155
|
+
return (_jsx(View, { style: styles.measureContainer, onLayout: onLayout, children: _jsx(View, { style: styles.hidden, children: children }) }));
|
|
156
|
+
}
|
|
157
|
+
// Build transform based on effect
|
|
158
|
+
const getTransform = () => {
|
|
159
|
+
const hasScale = ['scale', 'bounce', 'zoom', 'rotateScale', 'flip'].includes(effect);
|
|
160
|
+
const hasRotate = ['rotate', 'rotateScale'].includes(effect);
|
|
161
|
+
const hasFlip = effect === 'flip';
|
|
162
|
+
const hasTranslateX = ['slideLeft', 'slideRight'].includes(effect);
|
|
163
|
+
const hasTranslateY = ['slideUp', 'slideDown', 'bounce'].includes(effect);
|
|
164
|
+
const transforms = [];
|
|
165
|
+
if (hasScale) {
|
|
166
|
+
transforms.push({ scale });
|
|
167
|
+
}
|
|
168
|
+
if (hasRotate) {
|
|
169
|
+
transforms.push({
|
|
170
|
+
rotate: rotate.interpolate({
|
|
171
|
+
inputRange: [-360, 0, 360],
|
|
172
|
+
outputRange: ['-360deg', '0deg', '360deg'],
|
|
173
|
+
}),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
if (hasFlip) {
|
|
177
|
+
transforms.push({
|
|
178
|
+
rotateY: rotate.interpolate({
|
|
179
|
+
inputRange: [-180, 0],
|
|
180
|
+
outputRange: ['-180deg', '0deg'],
|
|
181
|
+
}),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
if (hasTranslateX) {
|
|
185
|
+
transforms.push({ translateX });
|
|
186
|
+
}
|
|
187
|
+
if (hasTranslateY) {
|
|
188
|
+
transforms.push({ translateY });
|
|
189
|
+
}
|
|
190
|
+
return transforms.length > 0 ? transforms : undefined;
|
|
191
|
+
};
|
|
192
|
+
const animatedStyle = {
|
|
193
|
+
opacity,
|
|
194
|
+
transform: getTransform(),
|
|
195
|
+
};
|
|
196
|
+
// Container style for non-skeleton mode
|
|
197
|
+
const containerAnimatedStyle = !skeleton && measuredHeight !== null
|
|
198
|
+
? { height: animatedHeight, overflow: 'hidden' }
|
|
199
|
+
: {};
|
|
200
|
+
return (_jsx(Animated.View, { style: [styles.container, style, containerAnimatedStyle], children: _jsx(Animated.View, { style: animatedStyle, children: children }) }));
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Get initial scale value based on effect
|
|
204
|
+
*/
|
|
205
|
+
function getInitialScale(effect) {
|
|
206
|
+
switch (effect) {
|
|
207
|
+
case 'scale':
|
|
208
|
+
case 'rotateScale':
|
|
209
|
+
return 0;
|
|
210
|
+
case 'bounce':
|
|
211
|
+
return 0.3;
|
|
212
|
+
case 'zoom':
|
|
213
|
+
return 0.5;
|
|
214
|
+
case 'flip':
|
|
215
|
+
return 0.8;
|
|
216
|
+
default:
|
|
217
|
+
return 1;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Get initial rotate value based on effect
|
|
222
|
+
*/
|
|
223
|
+
function getInitialRotate(effect) {
|
|
224
|
+
switch (effect) {
|
|
225
|
+
case 'rotate':
|
|
226
|
+
return -360;
|
|
227
|
+
case 'rotateScale':
|
|
228
|
+
return -180;
|
|
229
|
+
case 'flip':
|
|
230
|
+
return -180;
|
|
231
|
+
default:
|
|
232
|
+
return 0;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Get initial translateX value based on effect
|
|
237
|
+
*/
|
|
238
|
+
function getInitialTranslateX(effect) {
|
|
239
|
+
switch (effect) {
|
|
240
|
+
case 'slideLeft':
|
|
241
|
+
return 100;
|
|
242
|
+
case 'slideRight':
|
|
243
|
+
return -100;
|
|
244
|
+
default:
|
|
245
|
+
return 0;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Get initial translateY value based on effect
|
|
250
|
+
*/
|
|
251
|
+
function getInitialTranslateY(effect) {
|
|
252
|
+
switch (effect) {
|
|
253
|
+
case 'slideUp':
|
|
254
|
+
return 50;
|
|
255
|
+
case 'slideDown':
|
|
256
|
+
return -50;
|
|
257
|
+
case 'bounce':
|
|
258
|
+
return 30;
|
|
259
|
+
default:
|
|
260
|
+
return 0;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const styles = StyleSheet.create({
|
|
264
|
+
container: {},
|
|
265
|
+
measureContainer: {
|
|
266
|
+
position: 'absolute',
|
|
267
|
+
opacity: 0,
|
|
268
|
+
pointerEvents: 'none',
|
|
269
|
+
},
|
|
270
|
+
hidden: {
|
|
271
|
+
opacity: 0,
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
export default PuffPop;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { type ReactNode, type ReactElement } from 'react';
|
|
2
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
/**
|
|
4
|
+
* Animation effect types for PuffPop
|
|
5
|
+
*/
|
|
6
|
+
export type PuffPopEffect = 'scale' | 'rotate' | 'fade' | 'slideUp' | 'slideDown' | 'slideLeft' | 'slideRight' | 'bounce' | 'flip' | 'zoom' | 'rotateScale';
|
|
7
|
+
/**
|
|
8
|
+
* Easing function types
|
|
9
|
+
*/
|
|
10
|
+
export type PuffPopEasing = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' | 'spring' | 'bounce';
|
|
11
|
+
export interface PuffPopProps {
|
|
12
|
+
/**
|
|
13
|
+
* Children to animate
|
|
14
|
+
*/
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
/**
|
|
17
|
+
* Animation effect type
|
|
18
|
+
* @default 'scale'
|
|
19
|
+
*/
|
|
20
|
+
effect?: PuffPopEffect;
|
|
21
|
+
/**
|
|
22
|
+
* Animation duration in milliseconds
|
|
23
|
+
* @default 400
|
|
24
|
+
*/
|
|
25
|
+
duration?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Delay before animation starts in milliseconds
|
|
28
|
+
* @default 0
|
|
29
|
+
*/
|
|
30
|
+
delay?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Easing function for animation
|
|
33
|
+
* @default 'easeOut'
|
|
34
|
+
*/
|
|
35
|
+
easing?: PuffPopEasing;
|
|
36
|
+
/**
|
|
37
|
+
* If true, reserves space for children before animation (skeleton mode)
|
|
38
|
+
* If false, children height starts at 0 and expands, pushing content below
|
|
39
|
+
* @default true
|
|
40
|
+
*/
|
|
41
|
+
skeleton?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Whether to trigger animation (set to true to animate)
|
|
44
|
+
* @default true
|
|
45
|
+
*/
|
|
46
|
+
visible?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Callback when animation completes
|
|
49
|
+
*/
|
|
50
|
+
onAnimationComplete?: () => void;
|
|
51
|
+
/**
|
|
52
|
+
* Custom style for the container
|
|
53
|
+
*/
|
|
54
|
+
style?: StyleProp<ViewStyle>;
|
|
55
|
+
/**
|
|
56
|
+
* Whether to animate on mount
|
|
57
|
+
* @default true
|
|
58
|
+
*/
|
|
59
|
+
animateOnMount?: boolean;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* PuffPop - Animate children with beautiful entrance effects
|
|
63
|
+
*/
|
|
64
|
+
export declare function PuffPop({ children, effect, duration, delay, easing, skeleton, visible, onAnimationComplete, style, animateOnMount, }: PuffPopProps): ReactElement;
|
|
65
|
+
export default PuffPop;
|
package/package.json
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-puff-pop",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "A React Native animation library for revealing children components with beautiful puff and pop effects",
|
|
5
|
+
"main": "./lib/module/index.js",
|
|
6
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"source": "./src/index.tsx",
|
|
10
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
11
|
+
"default": "./lib/module/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"lib",
|
|
18
|
+
"!**/__tests__",
|
|
19
|
+
"!**/__fixtures__",
|
|
20
|
+
"!**/__mocks__",
|
|
21
|
+
"!**/.*"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"example": "yarn workspace react-native-puff-pop-example",
|
|
25
|
+
"ios": "yarn workspace react-native-puff-pop-example ios",
|
|
26
|
+
"android": "yarn workspace react-native-puff-pop-example android",
|
|
27
|
+
"web": "yarn workspace react-native-puff-pop-example web",
|
|
28
|
+
"clean": "del-cli lib",
|
|
29
|
+
"prepare": "yarn clean && tsc -p tsconfig.build.json",
|
|
30
|
+
"typecheck": "tsc",
|
|
31
|
+
"lint": "eslint \"**/*.{js,ts,tsx}\"",
|
|
32
|
+
"test": "jest"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"react-native",
|
|
36
|
+
"ios",
|
|
37
|
+
"android",
|
|
38
|
+
"expo",
|
|
39
|
+
"animation",
|
|
40
|
+
"puff",
|
|
41
|
+
"pop",
|
|
42
|
+
"reveal",
|
|
43
|
+
"transition",
|
|
44
|
+
"entrance",
|
|
45
|
+
"effects",
|
|
46
|
+
"skeleton",
|
|
47
|
+
"react-native-puff-pop"
|
|
48
|
+
],
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/liveforownhappiness/react-native-puff-pop.git"
|
|
52
|
+
},
|
|
53
|
+
"author": "liveforownhappiness <liveforownhappiness@gmail.com> (https://github.com/liveforownhappiness)",
|
|
54
|
+
"license": "MIT",
|
|
55
|
+
"bugs": {
|
|
56
|
+
"url": "https://github.com/liveforownhappiness/react-native-puff-pop/issues"
|
|
57
|
+
},
|
|
58
|
+
"homepage": "https://github.com/liveforownhappiness/react-native-puff-pop#readme",
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"registry": "https://registry.npmjs.org/"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@types/react": "^19.1.12",
|
|
64
|
+
"del-cli": "^6.0.0",
|
|
65
|
+
"react": "19.1.0",
|
|
66
|
+
"react-native": "0.81.5",
|
|
67
|
+
"typescript": "^5.9.2"
|
|
68
|
+
},
|
|
69
|
+
"peerDependencies": {
|
|
70
|
+
"react": "*",
|
|
71
|
+
"react-native": "*"
|
|
72
|
+
},
|
|
73
|
+
"workspaces": [
|
|
74
|
+
"example"
|
|
75
|
+
],
|
|
76
|
+
"packageManager": "yarn@4.11.0",
|
|
77
|
+
"prettier": {
|
|
78
|
+
"quoteProps": "consistent",
|
|
79
|
+
"singleQuote": true,
|
|
80
|
+
"tabWidth": 2,
|
|
81
|
+
"trailingComma": "es5",
|
|
82
|
+
"useTabs": false
|
|
83
|
+
},
|
|
84
|
+
"create-react-native-library": {
|
|
85
|
+
"type": "library",
|
|
86
|
+
"languages": "js",
|
|
87
|
+
"version": "0.56.0"
|
|
88
|
+
}
|
|
89
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useEffect,
|
|
3
|
+
useRef,
|
|
4
|
+
useState,
|
|
5
|
+
useCallback,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
type ReactElement,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import {
|
|
10
|
+
View,
|
|
11
|
+
Animated,
|
|
12
|
+
StyleSheet,
|
|
13
|
+
Easing,
|
|
14
|
+
type LayoutChangeEvent,
|
|
15
|
+
type StyleProp,
|
|
16
|
+
type ViewStyle,
|
|
17
|
+
} from 'react-native';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Animation effect types for PuffPop
|
|
21
|
+
*/
|
|
22
|
+
export type PuffPopEffect =
|
|
23
|
+
| 'scale' // Scale from center (점에서 커지면서 나타남)
|
|
24
|
+
| 'rotate' // Rotate while appearing (회전하면서 나타남)
|
|
25
|
+
| 'fade' // Simple fade in
|
|
26
|
+
| 'slideUp' // Slide from bottom
|
|
27
|
+
| 'slideDown' // Slide from top
|
|
28
|
+
| 'slideLeft' // Slide from right
|
|
29
|
+
| 'slideRight' // Slide from left
|
|
30
|
+
| 'bounce' // Bounce effect with overshoot
|
|
31
|
+
| 'flip' // 3D flip effect
|
|
32
|
+
| 'zoom' // Zoom with slight overshoot
|
|
33
|
+
| 'rotateScale'; // Rotate + Scale combined
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Easing function types
|
|
37
|
+
*/
|
|
38
|
+
export type PuffPopEasing =
|
|
39
|
+
| 'linear'
|
|
40
|
+
| 'easeIn'
|
|
41
|
+
| 'easeOut'
|
|
42
|
+
| 'easeInOut'
|
|
43
|
+
| 'spring'
|
|
44
|
+
| 'bounce';
|
|
45
|
+
|
|
46
|
+
export interface PuffPopProps {
|
|
47
|
+
/**
|
|
48
|
+
* Children to animate
|
|
49
|
+
*/
|
|
50
|
+
children: ReactNode;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Animation effect type
|
|
54
|
+
* @default 'scale'
|
|
55
|
+
*/
|
|
56
|
+
effect?: PuffPopEffect;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Animation duration in milliseconds
|
|
60
|
+
* @default 400
|
|
61
|
+
*/
|
|
62
|
+
duration?: number;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Delay before animation starts in milliseconds
|
|
66
|
+
* @default 0
|
|
67
|
+
*/
|
|
68
|
+
delay?: number;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Easing function for animation
|
|
72
|
+
* @default 'easeOut'
|
|
73
|
+
*/
|
|
74
|
+
easing?: PuffPopEasing;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* If true, reserves space for children before animation (skeleton mode)
|
|
78
|
+
* If false, children height starts at 0 and expands, pushing content below
|
|
79
|
+
* @default true
|
|
80
|
+
*/
|
|
81
|
+
skeleton?: boolean;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Whether to trigger animation (set to true to animate)
|
|
85
|
+
* @default true
|
|
86
|
+
*/
|
|
87
|
+
visible?: boolean;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Callback when animation completes
|
|
91
|
+
*/
|
|
92
|
+
onAnimationComplete?: () => void;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Custom style for the container
|
|
96
|
+
*/
|
|
97
|
+
style?: StyleProp<ViewStyle>;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Whether to animate on mount
|
|
101
|
+
* @default true
|
|
102
|
+
*/
|
|
103
|
+
animateOnMount?: boolean;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get easing function based on type
|
|
108
|
+
*/
|
|
109
|
+
function getEasing(type: PuffPopEasing): (value: number) => number {
|
|
110
|
+
switch (type) {
|
|
111
|
+
case 'linear':
|
|
112
|
+
return Easing.linear;
|
|
113
|
+
case 'easeIn':
|
|
114
|
+
return Easing.in(Easing.ease);
|
|
115
|
+
case 'easeOut':
|
|
116
|
+
return Easing.out(Easing.ease);
|
|
117
|
+
case 'easeInOut':
|
|
118
|
+
return Easing.inOut(Easing.ease);
|
|
119
|
+
case 'spring':
|
|
120
|
+
return Easing.out(Easing.back(1.5));
|
|
121
|
+
case 'bounce':
|
|
122
|
+
return Easing.bounce;
|
|
123
|
+
default:
|
|
124
|
+
return Easing.out(Easing.ease);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* PuffPop - Animate children with beautiful entrance effects
|
|
130
|
+
*/
|
|
131
|
+
export function PuffPop({
|
|
132
|
+
children,
|
|
133
|
+
effect = 'scale',
|
|
134
|
+
duration = 400,
|
|
135
|
+
delay = 0,
|
|
136
|
+
easing = 'easeOut',
|
|
137
|
+
skeleton = true,
|
|
138
|
+
visible = true,
|
|
139
|
+
onAnimationComplete,
|
|
140
|
+
style,
|
|
141
|
+
animateOnMount = true,
|
|
142
|
+
}: PuffPopProps): ReactElement {
|
|
143
|
+
// Animation values
|
|
144
|
+
const opacity = useRef(new Animated.Value(animateOnMount ? 0 : 1)).current;
|
|
145
|
+
const scale = useRef(new Animated.Value(animateOnMount ? getInitialScale(effect) : 1)).current;
|
|
146
|
+
const rotate = useRef(new Animated.Value(animateOnMount ? getInitialRotate(effect) : 0)).current;
|
|
147
|
+
const translateX = useRef(new Animated.Value(animateOnMount ? getInitialTranslateX(effect) : 0)).current;
|
|
148
|
+
const translateY = useRef(new Animated.Value(animateOnMount ? getInitialTranslateY(effect) : 0)).current;
|
|
149
|
+
|
|
150
|
+
// For non-skeleton mode
|
|
151
|
+
const [measuredHeight, setMeasuredHeight] = useState<number | null>(null);
|
|
152
|
+
const animatedHeight = useRef(new Animated.Value(0)).current;
|
|
153
|
+
const hasAnimated = useRef(false);
|
|
154
|
+
|
|
155
|
+
// Handle layout measurement for non-skeleton mode
|
|
156
|
+
const onLayout = useCallback(
|
|
157
|
+
(event: LayoutChangeEvent) => {
|
|
158
|
+
if (!skeleton && measuredHeight === null) {
|
|
159
|
+
const { height } = event.nativeEvent.layout;
|
|
160
|
+
setMeasuredHeight(height);
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
[skeleton, measuredHeight]
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// Animate function
|
|
167
|
+
const animate = useCallback(
|
|
168
|
+
(toVisible: boolean) => {
|
|
169
|
+
const easingFn = getEasing(easing);
|
|
170
|
+
// When skeleton is false, we animate height which doesn't support native driver
|
|
171
|
+
// So we must use JS driver for all animations in that case
|
|
172
|
+
const useNative = skeleton;
|
|
173
|
+
const config = {
|
|
174
|
+
duration,
|
|
175
|
+
easing: easingFn,
|
|
176
|
+
useNativeDriver: useNative,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const animations: Animated.CompositeAnimation[] = [];
|
|
180
|
+
|
|
181
|
+
// Opacity animation
|
|
182
|
+
animations.push(
|
|
183
|
+
Animated.timing(opacity, {
|
|
184
|
+
toValue: toVisible ? 1 : 0,
|
|
185
|
+
...config,
|
|
186
|
+
})
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// Scale animation
|
|
190
|
+
if (['scale', 'bounce', 'zoom', 'rotateScale', 'flip'].includes(effect)) {
|
|
191
|
+
const targetScale = toVisible ? 1 : getInitialScale(effect);
|
|
192
|
+
animations.push(
|
|
193
|
+
Animated.timing(scale, {
|
|
194
|
+
toValue: targetScale,
|
|
195
|
+
...config,
|
|
196
|
+
easing: effect === 'bounce' ? Easing.bounce : easingFn,
|
|
197
|
+
})
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Rotate animation
|
|
202
|
+
if (['rotate', 'rotateScale', 'flip'].includes(effect)) {
|
|
203
|
+
const targetRotate = toVisible ? 0 : getInitialRotate(effect);
|
|
204
|
+
animations.push(
|
|
205
|
+
Animated.timing(rotate, {
|
|
206
|
+
toValue: targetRotate,
|
|
207
|
+
...config,
|
|
208
|
+
})
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// TranslateX animation
|
|
213
|
+
if (['slideLeft', 'slideRight'].includes(effect)) {
|
|
214
|
+
const targetX = toVisible ? 0 : getInitialTranslateX(effect);
|
|
215
|
+
animations.push(
|
|
216
|
+
Animated.timing(translateX, {
|
|
217
|
+
toValue: targetX,
|
|
218
|
+
...config,
|
|
219
|
+
})
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// TranslateY animation
|
|
224
|
+
if (['slideUp', 'slideDown', 'bounce'].includes(effect)) {
|
|
225
|
+
const targetY = toVisible ? 0 : getInitialTranslateY(effect);
|
|
226
|
+
animations.push(
|
|
227
|
+
Animated.timing(translateY, {
|
|
228
|
+
toValue: targetY,
|
|
229
|
+
...config,
|
|
230
|
+
})
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Height animation for non-skeleton mode
|
|
235
|
+
if (!skeleton && measuredHeight !== null) {
|
|
236
|
+
const targetHeight = toVisible ? measuredHeight : 0;
|
|
237
|
+
animations.push(
|
|
238
|
+
Animated.timing(animatedHeight, {
|
|
239
|
+
toValue: targetHeight,
|
|
240
|
+
duration,
|
|
241
|
+
easing: easingFn,
|
|
242
|
+
useNativeDriver: false,
|
|
243
|
+
})
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Run animations with delay
|
|
248
|
+
const parallelAnimation = Animated.parallel(animations);
|
|
249
|
+
|
|
250
|
+
if (delay > 0) {
|
|
251
|
+
Animated.sequence([
|
|
252
|
+
Animated.delay(delay),
|
|
253
|
+
parallelAnimation,
|
|
254
|
+
]).start(() => {
|
|
255
|
+
if (toVisible && onAnimationComplete) {
|
|
256
|
+
onAnimationComplete();
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
} else {
|
|
260
|
+
parallelAnimation.start(() => {
|
|
261
|
+
if (toVisible && onAnimationComplete) {
|
|
262
|
+
onAnimationComplete();
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
[
|
|
268
|
+
delay,
|
|
269
|
+
duration,
|
|
270
|
+
easing,
|
|
271
|
+
effect,
|
|
272
|
+
measuredHeight,
|
|
273
|
+
onAnimationComplete,
|
|
274
|
+
opacity,
|
|
275
|
+
rotate,
|
|
276
|
+
scale,
|
|
277
|
+
skeleton,
|
|
278
|
+
translateX,
|
|
279
|
+
translateY,
|
|
280
|
+
animatedHeight,
|
|
281
|
+
]
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
// Handle initial mount animation
|
|
285
|
+
useEffect(() => {
|
|
286
|
+
if (animateOnMount && !hasAnimated.current && visible) {
|
|
287
|
+
hasAnimated.current = true;
|
|
288
|
+
animate(true);
|
|
289
|
+
}
|
|
290
|
+
}, [animate, animateOnMount, visible]);
|
|
291
|
+
|
|
292
|
+
// Handle visibility changes after mount
|
|
293
|
+
useEffect(() => {
|
|
294
|
+
if (hasAnimated.current) {
|
|
295
|
+
animate(visible);
|
|
296
|
+
}
|
|
297
|
+
}, [visible, animate]);
|
|
298
|
+
|
|
299
|
+
// For non-skeleton mode, measure first
|
|
300
|
+
if (!skeleton && measuredHeight === null) {
|
|
301
|
+
return (
|
|
302
|
+
<View style={styles.measureContainer} onLayout={onLayout}>
|
|
303
|
+
<View style={styles.hidden}>{children}</View>
|
|
304
|
+
</View>
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Build transform based on effect
|
|
309
|
+
const getTransform = () => {
|
|
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);
|
|
315
|
+
|
|
316
|
+
const transforms = [];
|
|
317
|
+
|
|
318
|
+
if (hasScale) {
|
|
319
|
+
transforms.push({ scale });
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (hasRotate) {
|
|
323
|
+
transforms.push({
|
|
324
|
+
rotate: rotate.interpolate({
|
|
325
|
+
inputRange: [-360, 0, 360],
|
|
326
|
+
outputRange: ['-360deg', '0deg', '360deg'],
|
|
327
|
+
}),
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (hasFlip) {
|
|
332
|
+
transforms.push({
|
|
333
|
+
rotateY: rotate.interpolate({
|
|
334
|
+
inputRange: [-180, 0],
|
|
335
|
+
outputRange: ['-180deg', '0deg'],
|
|
336
|
+
}),
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (hasTranslateX) {
|
|
341
|
+
transforms.push({ translateX });
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (hasTranslateY) {
|
|
345
|
+
transforms.push({ translateY });
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return transforms.length > 0 ? transforms : undefined;
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const animatedStyle = {
|
|
352
|
+
opacity,
|
|
353
|
+
transform: getTransform(),
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// Container style for non-skeleton mode
|
|
357
|
+
const containerAnimatedStyle = !skeleton && measuredHeight !== null
|
|
358
|
+
? { height: animatedHeight, overflow: 'hidden' as const }
|
|
359
|
+
: {};
|
|
360
|
+
|
|
361
|
+
return (
|
|
362
|
+
<Animated.View style={[styles.container, style, containerAnimatedStyle]}>
|
|
363
|
+
<Animated.View style={animatedStyle}>{children}</Animated.View>
|
|
364
|
+
</Animated.View>
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Get initial scale value based on effect
|
|
370
|
+
*/
|
|
371
|
+
function getInitialScale(effect: PuffPopEffect): number {
|
|
372
|
+
switch (effect) {
|
|
373
|
+
case 'scale':
|
|
374
|
+
case 'rotateScale':
|
|
375
|
+
return 0;
|
|
376
|
+
case 'bounce':
|
|
377
|
+
return 0.3;
|
|
378
|
+
case 'zoom':
|
|
379
|
+
return 0.5;
|
|
380
|
+
case 'flip':
|
|
381
|
+
return 0.8;
|
|
382
|
+
default:
|
|
383
|
+
return 1;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Get initial rotate value based on effect
|
|
389
|
+
*/
|
|
390
|
+
function getInitialRotate(effect: PuffPopEffect): number {
|
|
391
|
+
switch (effect) {
|
|
392
|
+
case 'rotate':
|
|
393
|
+
return -360;
|
|
394
|
+
case 'rotateScale':
|
|
395
|
+
return -180;
|
|
396
|
+
case 'flip':
|
|
397
|
+
return -180;
|
|
398
|
+
default:
|
|
399
|
+
return 0;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Get initial translateX value based on effect
|
|
405
|
+
*/
|
|
406
|
+
function getInitialTranslateX(effect: PuffPopEffect): number {
|
|
407
|
+
switch (effect) {
|
|
408
|
+
case 'slideLeft':
|
|
409
|
+
return 100;
|
|
410
|
+
case 'slideRight':
|
|
411
|
+
return -100;
|
|
412
|
+
default:
|
|
413
|
+
return 0;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Get initial translateY value based on effect
|
|
419
|
+
*/
|
|
420
|
+
function getInitialTranslateY(effect: PuffPopEffect): number {
|
|
421
|
+
switch (effect) {
|
|
422
|
+
case 'slideUp':
|
|
423
|
+
return 50;
|
|
424
|
+
case 'slideDown':
|
|
425
|
+
return -50;
|
|
426
|
+
case 'bounce':
|
|
427
|
+
return 30;
|
|
428
|
+
default:
|
|
429
|
+
return 0;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const styles = StyleSheet.create({
|
|
434
|
+
container: {},
|
|
435
|
+
measureContainer: {
|
|
436
|
+
position: 'absolute',
|
|
437
|
+
opacity: 0,
|
|
438
|
+
pointerEvents: 'none',
|
|
439
|
+
},
|
|
440
|
+
hidden: {
|
|
441
|
+
opacity: 0,
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
export default PuffPop;
|
|
446
|
+
|