1- import React , { forwardRef , useEffect } from "react" ;
2- import { ViewProps } from "react-native" ;
3- import {
1+ import React , { forwardRef } from "react" ;
2+ import { ColorValue } from "react-native" ;
3+ import Animated , {
4+ cancelAnimation ,
45 Easing ,
6+ interpolate ,
7+ useAnimatedProps ,
58 useAnimatedStyle ,
69 useSharedValue ,
710 withRepeat ,
811 withTiming ,
912} from "react-native-reanimated" ;
13+ import Svg , { Circle , Defs , G , LinearGradient , Stop } from "react-native-svg" ;
1014
11- import { AnimatedBox } from "../../primitives" ;
15+ import { AnimatedBox , BoxProps } from "../../primitives" ;
1216import { useTheme } from "../../theme" ;
13- import { createComponent , cx , styleAdapter } from "../../utils" ;
17+ import { createComponent , styleAdapter } from "../../utils" ;
18+
19+ const AnimatedCircle = Animated . createAnimatedComponent ( Circle ) ;
1420
1521export type SpinnerSizes = "xs" | "sm" | "md" | "lg" | "xl" ;
1622export type SpinnerTheme =
@@ -20,7 +26,7 @@ export type SpinnerTheme =
2026 | "success"
2127 | "danger" ;
2228
23- export interface SpinnerLibProps {
29+ export interface SpinnerProps extends BoxProps {
2430 /**
2531 * How large should the spinner be?
2632 * @default md
@@ -38,53 +44,130 @@ export interface SpinnerLibProps {
3844 themeColor : SpinnerTheme ;
3945}
4046
41- export interface SpinnerProps extends SpinnerLibProps , ViewProps { }
42-
4347const RNSpinner : React . FC < Partial < SpinnerProps > > = forwardRef <
4448 typeof AnimatedBox ,
4549 Partial < SpinnerProps >
46- > ( ( { size = "md" , track = "transparent" , themeColor = "base" , style } , ref ) => {
47- const spinnerLoopAnimation = useSharedValue ( 0 ) ;
48- useEffect ( ( ) => {
49- spinnerLoopAnimation . value = withRepeat (
50- withTiming ( 360 , {
51- duration : 1000 ,
52- easing : Easing . linear ,
53- } ) ,
54- - 1 ,
55- false ,
56- ) ;
57- } , [ spinnerLoopAnimation ] ) ;
58- const spinnerLoadingStyle = useAnimatedStyle ( ( ) => {
50+ > ( ( props , ref ) => {
51+ const tailwind = useTheme ( ) ;
52+ const spinnerTheme = useTheme ( "spinner" ) ;
53+
54+ const {
55+ size = "md" ,
56+ track = "transparent" ,
57+ themeColor = "base" ,
58+ style,
59+ } = props ;
60+
61+ // Circle parameters
62+ const radius = 44 ;
63+
64+ const progress = useSharedValue ( 0 ) ;
65+ const rotate = useSharedValue ( 0 ) ;
66+
67+ const animatedSvgStyle = useAnimatedStyle ( ( ) => {
68+ const rotateValue = interpolate ( rotate . value , [ 0 , 1 ] , [ 0 , 360 ] ) ;
5969 return {
6070 transform : [
6171 {
62- rotate : `${ spinnerLoopAnimation . value } deg` ,
72+ rotate : `${ rotateValue } deg` ,
6373 } ,
6474 ] ,
6575 } ;
6676 } ) ;
6777
68- const tailwind = useTheme ( ) ;
69- const spinnerTheme = useTheme ( "spinner" ) ;
78+ const indeterminateAnimatedCircularProgress = useAnimatedProps ( ( ) => {
79+ return {
80+ strokeDashoffset : interpolate (
81+ progress . value ,
82+ [ 0 , 0.5 , 1 ] ,
83+ [ 0 , - 276 , - ( 276 * 2 ) ] ,
84+ ) ,
85+ } ;
86+ } ) ;
87+
88+ React . useEffect ( ( ) => {
89+ progress . value = withRepeat (
90+ withTiming ( 1 , {
91+ duration : 1500 ,
92+ easing : Easing . linear ,
93+ } ) ,
94+ - 1 ,
95+ false ,
96+ finished => {
97+ if ( ! finished ) {
98+ progress . value = 0 ;
99+ }
100+ } ,
101+ ) ;
102+ rotate . value = withRepeat (
103+ withTiming ( 1 , {
104+ duration : 1000 ,
105+ easing : Easing . bezier ( 0.4 , 0 , 0.2 , 1 ) ,
106+ } ) ,
107+ - 1 ,
108+ false ,
109+ finished => {
110+ if ( ! finished ) {
111+ rotate . value = 0 ;
112+ }
113+ } ,
114+ ) ;
115+ return ( ) => {
116+ cancelAnimation ( progress ) ;
117+ cancelAnimation ( rotate ) ;
118+ } ;
119+ } , [ progress , rotate ] ) ;
70120 return (
71121 < AnimatedBox
72122 ref = { ref }
73123 style = { [
74- tailwind . style (
75- cx (
76- spinnerTheme . base ,
77- spinnerTheme . themeColor [ themeColor ] ,
78- track === "visible"
79- ? spinnerTheme . track [ track ] ?. [ themeColor ]
80- : spinnerTheme . track . transparent ,
81- spinnerTheme . size [ size ] ,
82- ) ,
83- ) ,
84- styleAdapter ( style ) , // Accepts View Style to overide the Default Spinner Style
85- spinnerLoadingStyle ,
124+ tailwind . style ( spinnerTheme . size [ size ] ) ,
125+ styleAdapter ( style ) ,
126+ animatedSvgStyle ,
86127 ] }
87- />
128+ >
129+ < Svg width = "100%" height = "100%" viewBox = { "0 0 100 100" } >
130+ < Defs >
131+ < LinearGradient id = "gradient" x1 = "0%" y1 = "0%" x2 = "100%" y2 = "100%" >
132+ < Stop
133+ offset = "0%"
134+ stopColor = { tailwind . getColor ( spinnerTheme . themeColor [ themeColor ] ) }
135+ />
136+ < Stop
137+ offset = "100%"
138+ stopColor = { tailwind . getColor ( spinnerTheme . themeColor [ themeColor ] ) }
139+ stopOpacity = "0"
140+ />
141+ </ LinearGradient >
142+ </ Defs >
143+ < G rotation = { "-90" } origin = "50, 50" >
144+ < Circle
145+ stroke = {
146+ tailwind . getColor (
147+ track === "transparent"
148+ ? spinnerTheme . track [ track ]
149+ : spinnerTheme . track [ track ] [ themeColor ] ,
150+ ) as ColorValue
151+ }
152+ strokeWidth = { 10 }
153+ fill = "transparent"
154+ r = { radius }
155+ cx = { 50 }
156+ cy = { 50 }
157+ />
158+ < AnimatedCircle
159+ stroke = "url(#gradient)"
160+ strokeWidth = { 10 }
161+ r = { radius }
162+ cx = { 50 }
163+ cy = { 50 }
164+ strokeLinecap = "round"
165+ strokeDasharray = "276 276"
166+ animatedProps = { indeterminateAnimatedCircularProgress }
167+ />
168+ </ G >
169+ </ Svg >
170+ </ AnimatedBox >
88171 ) ;
89172} ) ;
90173
0 commit comments