template_0205
This commit is contained in:
120
RN_TEMPLATE/app/components/Skeleton.tsx
Normal file
120
RN_TEMPLATE/app/components/Skeleton.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user