Skip to content

Commit 2f30afd

Browse files
committed
feat(skeleton): handle entering and exiting
1 parent 742217c commit 2f30afd

File tree

4 files changed

+152
-80
lines changed

4 files changed

+152
-80
lines changed

example/src/app/(home)/components/skeleton.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Avatar, Button, Card, Skeleton } from 'heroui-native';
22
import { useState } from 'react';
33
import { ScrollView, Text, View } from 'react-native';
4+
import { FadeInLeft, FadeOutRight } from 'react-native-reanimated';
45

56
export default function SkeletonScreen() {
67
const [isLoading, setIsLoading] = useState(true);
@@ -46,6 +47,8 @@ export default function SkeletonScreen() {
4647

4748
<View className="w-full gap-3 mb-6">
4849
<Skeleton
50+
entering={FadeInLeft}
51+
exiting={FadeOutRight}
4952
className="h-4 w-full rounded-md"
5053
isLoading={isLoading}
5154
animationType={animationType}
@@ -56,6 +59,8 @@ export default function SkeletonScreen() {
5659
</Skeleton>
5760

5861
<Skeleton
62+
entering={FadeInLeft}
63+
exiting={FadeOutRight}
5964
className="h-4 w-3/4 rounded-md"
6065
isLoading={isLoading}
6166
animationType={animationType}
@@ -64,6 +69,8 @@ export default function SkeletonScreen() {
6469
</Skeleton>
6570

6671
<Skeleton
72+
entering={FadeInLeft}
73+
exiting={FadeOutRight}
6774
className="h-4 w-1/2 rounded-md"
6875
isLoading={isLoading}
6976
animationType={animationType}

src/components/skeleton/skeleton.styles.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
import { StyleSheet } from 'react-native';
12
import { tv } from 'tailwind-variants';
23
import { combineStyles } from '../../providers/theme/helpers';
34

45
/**
56
* Main skeleton component styles
67
*/
78
const skeleton = tv({
8-
base: 'bg-muted overflow-hidden',
9+
base: 'bg-muted/70 overflow-hidden',
910
});
1011

1112
/**
@@ -15,6 +16,15 @@ const gradientWrapper = tv({
1516
base: 'absolute inset-0',
1617
});
1718

19+
/**
20+
* Native styles for border curve
21+
*/
22+
export const nativeStyles = StyleSheet.create({
23+
borderCurve: {
24+
borderCurve: 'continuous',
25+
},
26+
});
27+
1828
/**
1929
* Combined skeleton styles
2030
*/

src/components/skeleton/skeleton.tsx

Lines changed: 132 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@ import {
77
import Animated, {
88
cancelAnimation,
99
Easing,
10+
FadeIn,
11+
FadeOut,
1012
interpolate,
1113
ReduceMotion,
1214
useAnimatedStyle,
1315
useSharedValue,
1416
withRepeat,
1517
withTiming,
18+
type SharedValue,
1619
} from 'react-native-reanimated';
1720

1821
import { colorKit, useTheme } from '../../providers/theme';
@@ -29,12 +32,115 @@ import {
2932
DEFAULT_SPEED,
3033
DISPLAY_NAME,
3134
} from './skeleton.constants';
32-
import skeletonStyles from './skeleton.styles';
33-
import type { SkeletonProps } from './skeleton.types';
35+
import skeletonStyles, { nativeStyles } from './skeleton.styles';
36+
import type {
37+
PulseConfig,
38+
ShimmerConfig,
39+
SkeletonProps,
40+
} from './skeleton.types';
41+
42+
// --------------------------------------------------
43+
44+
interface ShimmerAnimationProps {
45+
isLoading: boolean;
46+
shimmerConfig?: ShimmerConfig;
47+
componentWidth: number;
48+
offset: number;
49+
progress: SharedValue<number>;
50+
screenWidth: number;
51+
isDark: boolean;
52+
colors: any;
53+
}
54+
55+
const ShimmerAnimation: React.FC<ShimmerAnimationProps> = ({
56+
shimmerConfig,
57+
componentWidth,
58+
offset,
59+
progress,
60+
screenWidth,
61+
isDark,
62+
colors,
63+
}) => {
64+
const highlightColor = isDark
65+
? colorKit
66+
.setAlpha(colorKit.increaseBrightness(colors.background, 10).hex(), 0.5)
67+
.hex()
68+
: colorKit
69+
.setAlpha(colorKit.decreaseBrightness(colors.background, 5).hex(), 0.5)
70+
.hex();
71+
72+
const gradientColors = shimmerConfig?.gradientConfig?.colors ?? [
73+
'transparent',
74+
highlightColor,
75+
'transparent',
76+
];
77+
const gradientStart =
78+
shimmerConfig?.gradientConfig?.start ?? DEFAULT_GRADIENT_START;
79+
const gradientEnd =
80+
shimmerConfig?.gradientConfig?.end ?? DEFAULT_GRADIENT_END;
81+
82+
const shimmerAnimatedStyle = useAnimatedStyle(() => {
83+
const translateX = interpolate(
84+
progress.get(),
85+
[0, 1],
86+
[-(componentWidth + offset), screenWidth]
87+
);
88+
89+
return {
90+
opacity: 1,
91+
transform: [{ translateX }],
92+
};
93+
});
94+
95+
return (
96+
<Animated.View
97+
style={[
98+
StyleSheet.absoluteFill,
99+
nativeStyles.borderCurve,
100+
shimmerAnimatedStyle,
101+
]}
102+
>
103+
<LinearGradientComponent
104+
colors={gradientColors}
105+
start={gradientStart}
106+
end={gradientEnd}
107+
style={StyleSheet.absoluteFill}
108+
/>
109+
</Animated.View>
110+
);
111+
};
112+
113+
// --------------------------------------------------
114+
115+
interface PulseAnimationProps {
116+
pulseConfig?: PulseConfig;
117+
progress: SharedValue<number>;
118+
}
119+
120+
const usePulseAnimation = ({ pulseConfig, progress }: PulseAnimationProps) => {
121+
const pulseMinOpacity = pulseConfig?.minOpacity ?? DEFAULT_PULSE_MIN_OPACITY;
122+
const pulseMaxOpacity = pulseConfig?.maxOpacity ?? DEFAULT_PULSE_MAX_OPACITY;
123+
124+
return useAnimatedStyle(() => {
125+
const opacity = interpolate(
126+
progress.get(),
127+
[0, 1],
128+
[pulseMinOpacity, pulseMaxOpacity]
129+
);
130+
131+
return {
132+
opacity,
133+
};
134+
});
135+
};
136+
137+
// --------------------------------------------------
34138

35139
const Skeleton: React.FC<SkeletonProps> = (props) => {
36140
const {
37141
children,
142+
entering = FadeIn,
143+
exiting = FadeOut,
38144
isLoading = true,
39145
animationType = 'shimmer',
40146
shimmerConfig,
@@ -49,41 +155,18 @@ const Skeleton: React.FC<SkeletonProps> = (props) => {
49155
const progress = useSharedValue(0);
50156

51157
const { width: SCREEN_WIDTH } = useWindowDimensions();
52-
53158
const { colors, isDark } = useTheme();
54159

55160
const shimmerDuration = shimmerConfig?.duration ?? DEFAULT_SHIMMER_DURATION;
56161
const shimmerEasing = shimmerConfig?.easing ?? DEFAULT_EASING;
57162
const shimmerSpeed = shimmerConfig?.speed ?? DEFAULT_SPEED;
58163

59-
const highlightColor = isDark
60-
? colorKit
61-
.setAlpha(colorKit.increaseBrightness(colors.background, 10).hex(), 0.5)
62-
.hex()
63-
: colorKit
64-
.setAlpha(colorKit.decreaseBrightness(colors.background, 5).hex(), 0.5)
65-
.hex();
66-
67-
const gradientColors = shimmerConfig?.gradientConfig?.colors ?? [
68-
'transparent',
69-
highlightColor,
70-
'transparent',
71-
];
72-
const gradientStart =
73-
shimmerConfig?.gradientConfig?.start ?? DEFAULT_GRADIENT_START;
74-
const gradientEnd =
75-
shimmerConfig?.gradientConfig?.end ?? DEFAULT_GRADIENT_END;
76-
77-
// Pulse configuration
78164
const pulseDuration = pulseConfig?.duration ?? DEFAULT_PULSE_DURATION;
79165
const pulseEasing = pulseConfig?.easing ?? Easing.inOut(Easing.ease);
80-
const pulseMinOpacity = pulseConfig?.minOpacity ?? DEFAULT_PULSE_MIN_OPACITY;
81-
const pulseMaxOpacity = pulseConfig?.maxOpacity ?? DEFAULT_PULSE_MAX_OPACITY;
82166

83-
// 5. Styles
84167
const tvStyles = skeletonStyles.skeleton({ className });
168+
const pulseAnimatedStyle = usePulseAnimation({ pulseConfig, progress });
85169

86-
// 6. Effects
87170
useEffect(() => {
88171
if (isLoading && animationType !== 'none') {
89172
progress.set(0);
@@ -130,46 +213,6 @@ const Skeleton: React.FC<SkeletonProps> = (props) => {
130213
pulseEasing,
131214
]);
132215

133-
// 7. Animation styles
134-
const shimmerAnimatedStyle = useAnimatedStyle(() => {
135-
if (animationType !== 'shimmer' || !isLoading) {
136-
return {
137-
opacity: 0,
138-
transform: [{ translateX: 0 }],
139-
};
140-
}
141-
142-
const translateX = interpolate(
143-
progress.get(),
144-
[0, 1],
145-
[-(componentWidth + offset), SCREEN_WIDTH]
146-
);
147-
148-
return {
149-
opacity: 1,
150-
transform: [{ translateX }],
151-
};
152-
});
153-
154-
const pulseAnimatedStyle = useAnimatedStyle(() => {
155-
if (animationType !== 'pulse' || !isLoading) {
156-
return {
157-
opacity: 1,
158-
};
159-
}
160-
161-
const opacity = interpolate(
162-
progress.get(),
163-
[0, 1],
164-
[pulseMinOpacity, pulseMaxOpacity]
165-
);
166-
167-
return {
168-
opacity,
169-
};
170-
});
171-
172-
// 8. Callbacks
173216
const handleLayout = useCallback(
174217
(event: LayoutChangeEvent) => {
175218
if (componentWidth === 0) {
@@ -181,27 +224,39 @@ const Skeleton: React.FC<SkeletonProps> = (props) => {
181224
[componentWidth]
182225
);
183226

184-
// 9. Render
185227
if (!isLoading) {
186-
return <>{children}</>;
228+
return (
229+
<Animated.View key="content" entering={entering} exiting={exiting}>
230+
{children}
231+
</Animated.View>
232+
);
187233
}
188234

189235
return (
190236
<Animated.View
237+
key="skeleton"
238+
entering={entering}
239+
exiting={exiting}
191240
onLayout={handleLayout}
192-
style={[animationType === 'pulse' && pulseAnimatedStyle, style]}
241+
style={[
242+
animationType === 'pulse' && pulseAnimatedStyle,
243+
nativeStyles.borderCurve,
244+
style,
245+
]}
193246
className={tvStyles}
194247
{...restProps}
195248
>
196249
{animationType === 'shimmer' && componentWidth > 0 && (
197-
<Animated.View style={[StyleSheet.absoluteFill, shimmerAnimatedStyle]}>
198-
<LinearGradientComponent
199-
colors={gradientColors}
200-
start={gradientStart}
201-
end={gradientEnd}
202-
style={StyleSheet.absoluteFill}
203-
/>
204-
</Animated.View>
250+
<ShimmerAnimation
251+
isLoading={isLoading}
252+
shimmerConfig={shimmerConfig}
253+
componentWidth={componentWidth}
254+
offset={offset}
255+
progress={progress}
256+
screenWidth={SCREEN_WIDTH}
257+
isDark={isDark}
258+
colors={colors}
259+
/>
205260
)}
206261
</Animated.View>
207262
);

src/components/skeleton/skeleton.types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ViewProps } from 'react-native';
2-
import type { EasingFunction } from 'react-native-reanimated';
2+
import type { AnimatedProps, EasingFunction } from 'react-native-reanimated';
33

44
/**
55
* Skeleton animation type - defines the animation style
@@ -87,7 +87,7 @@ export interface PulseConfig {
8787
/**
8888
* Props for the main Skeleton component
8989
*/
90-
export interface SkeletonProps extends ViewProps {
90+
export interface SkeletonProps extends AnimatedProps<ViewProps> {
9191
/**
9292
* Child components to show when not loading
9393
*/

0 commit comments

Comments
 (0)