import { Canvas, DashPathEffect, Path, Shadow, Skia, SweepGradient, vec, } from "@shopify/react-native-skia"; import React, { useEffect } from "react"; import { StyleSheet, TextInput, View, ViewStyle } from "react-native"; import Animated, { Easing, useAnimatedProps, useDerivedValue, useSharedValue, withTiming, } from "react-native-reanimated"; import { useTheme } from "../../theme"; const AnimatedTextInput = Animated.createAnimatedComponent(TextInput); interface CircularProgressBarProps { size?: number; strokeWidth?: number; backgroundColor?: string; progressColors?: string[]; lineCap?: "butt" | "round" | "square"; customText?: string; textColor?: string; fontSize?: number; style?: ViewStyle; innerShadowColor?: string; duration?: number; variant?: "progress" | "steps"; progress?: number; progressPadding?: number; currentStep?: number; totalSteps?: number; stepGap?: number; fontFamily?: string; } const CircularProgressBar: React.FC = ({ size = 120, strokeWidth = 8, backgroundColor, progressColors, lineCap = "round", customText, textColor, fontSize = size / 4, style, innerShadowColor, duration = 1000, variant = "progress", progress = 0, progressPadding = 0, currentStep = 0, totalSteps = 4, stepGap = 15, fontFamily, }) => { const { theme } = useTheme(); const animatedValue = useSharedValue(0); const finalColors = progressColors || [ theme.primary, theme.primaryVariant, theme.primary, ]; const finalBackgroundColor = backgroundColor || theme.disabled; const finalTextColor = textColor || theme.onSurface; const radius = (size - strokeWidth) / 2; const circumference = 2 * Math.PI * radius; const center = vec(size / 2, size / 2); const path = Skia.Path.Make(); path.addCircle(size / 2, size / 2, radius); const progressStrokeWidth = Math.max(0, strokeWidth - progressPadding * 2); useEffect(() => { const targetValue = variant === "steps" ? currentStep / (totalSteps || 1) : progress / 100; animatedValue.value = withTiming(targetValue, { duration, easing: Easing.out(Easing.cubic), }); }, [progress, currentStep, totalSteps, duration, variant, animatedValue]); const textToDisplay = useDerivedValue(() => { if (customText !== undefined) { return customText; } if (variant === "steps") { const displayStep = Math.round(animatedValue.value * (totalSteps || 1)); return `${displayStep}/${totalSteps}`; } return `${Math.round(animatedValue.value * 100)}%`; }, [customText, variant, animatedValue, totalSteps]); const animatedProps = useAnimatedProps(() => { return { text: textToDisplay.value, } as any; }); const getInitialText = () => { if (customText !== undefined) return customText; if (variant === "steps") return `${currentStep}/${totalSteps}`; return `${progress}%`; }; const renderSteps = () => { const totalGapDegrees = (totalSteps || 1) * stepGap; const totalArcDegrees = 360 - totalGapDegrees; const stepArcLength = (totalArcDegrees / (totalSteps || 1) / 360) * circumference; const gapLength = (stepGap / 360) * circumference; const dashPattern = [stepArcLength, gapLength]; return ( <> {innerShadowColor && ( )} ); }; const renderProgress = () => ( <> {innerShadowColor && ( )} ); return ( {variant === "steps" ? renderSteps() : renderProgress()} ); }; export default CircularProgressBar;