Files
RN_Template/RN_TEMPLATE/app/components/Skeleton.tsx
2026-02-05 13:16:05 +08:00

121 lines
2.9 KiB
TypeScript

import { createContext, useContext, useEffect } from "react"
import { StyleProp, View, ViewStyle } from "react-native"
import Animated, {
SharedValue,
useAnimatedStyle,
useSharedValue,
withRepeat,
withTiming,
Easing,
interpolate,
} from "react-native-reanimated"
import { useAppTheme } from "@/theme/context"
import { s } from "@/utils/responsive"
// Shared animation context for synchronized pulse
const PulseContext = createContext<SharedValue<number> | null>(null)
export interface SkeletonProps {
/**
* Width of the skeleton. Can be number or percentage string.
* @default "100%"
*/
width?: number | `${number}%`
/**
* Height of the skeleton.
* @default 16
*/
height?: number
/**
* Border radius. Use "round" for circle.
* @default 4
*/
radius?: number | "round"
/**
* Style overrides
*/
style?: StyleProp<ViewStyle>
}
/**
* A skeleton placeholder with pulse animation.
* When used inside SkeletonContainer, all skeletons animate in sync.
*/
export function Skeleton(props: SkeletonProps) {
const { width = "100%", height = s(16), radius = s(4), style } = props
const { theme } = useAppTheme()
// Use shared animation from context, or create own
const sharedProgress = useContext(PulseContext)
const localProgress = useSharedValue(0)
const progress = sharedProgress || localProgress
// Only start local animation if not using shared context
useEffect(() => {
if (!sharedProgress) {
localProgress.value = withRepeat(
withTiming(1, { duration: 1000, easing: Easing.inOut(Easing.ease) }),
-1,
true,
)
}
}, [sharedProgress, localProgress])
const animatedStyle = useAnimatedStyle(() => ({
opacity: interpolate(progress.value, [0, 1], [0.4, 1]),
}))
const borderRadius = radius === "round" ? (typeof height === "number" ? height / 2 : 999) : radius
return (
<Animated.View
style={[
{ width, height, borderRadius, backgroundColor: theme.colors.border },
animatedStyle,
style,
]}
/>
)
}
export interface SkeletonContainerProps {
/**
* Children (Skeleton components)
*/
children: React.ReactNode
/**
* Style overrides for container
*/
style?: StyleProp<ViewStyle>
}
/**
* Container that synchronizes pulse animation across all child Skeletons.
*
* @example
* <SkeletonContainer>
* <Skeleton width="60%" height={14} />
* <Skeleton width={40} height={40} radius="round" />
* </SkeletonContainer>
*/
export function SkeletonContainer(props: SkeletonContainerProps) {
const { children, style } = props
const progress = useSharedValue(0)
useEffect(() => {
progress.value = withRepeat(
withTiming(1, { duration: 1000, easing: Easing.inOut(Easing.ease) }),
-1,
true,
)
}, [progress])
return (
<PulseContext.Provider value={progress}>
<View style={style}>{children}</View>
</PulseContext.Provider>
)
}