1- import React , { useEffect , useMemo , useRef } from 'react' ;
2- import type { ColorValue } from 'react-native' ;
3- import { Animated , Easing , StyleProp , ViewStyle } from 'react-native' ;
1+ import React , { useEffect , useMemo } from 'react' ;
2+ import type { ColorValue , StyleProp , ViewStyle } from 'react-native' ;
3+ import Animated , {
4+ cancelAnimation ,
5+ Easing ,
6+ useAnimatedProps ,
7+ useAnimatedStyle ,
8+ useSharedValue ,
9+ withRepeat ,
10+ withTiming ,
11+ } from 'react-native-reanimated' ;
412import Svg , { Circle } from 'react-native-svg' ;
513
14+ const AnimatedCircle = Animated . createAnimatedComponent ( Circle ) ;
15+ const SPIN_DURATION_MS = 900 ;
16+ const PROGRESS_ANIMATION_DURATION_MS = 1200 ;
17+
618export type CircularProgressIndicatorProps = {
719 /** Upload percent **0–100**. */
820 progress : number ;
@@ -24,32 +36,8 @@ export const CircularProgressIndicator = ({
2436 style,
2537 testID,
2638} : CircularProgressIndicatorProps ) => {
27- const spin = useRef ( new Animated . Value ( 0 ) ) . current ;
28-
29- useEffect ( ( ) => {
30- const loop = Animated . loop (
31- Animated . timing ( spin , {
32- toValue : 1 ,
33- duration : 900 ,
34- easing : Easing . linear ,
35- useNativeDriver : true ,
36- } ) ,
37- ) ;
38- loop . start ( ) ;
39- return ( ) => {
40- loop . stop ( ) ;
41- spin . setValue ( 0 ) ;
42- } ;
43- } , [ progress , spin ] ) ;
44-
45- const rotate = useMemo (
46- ( ) =>
47- spin . interpolate ( {
48- inputRange : [ 0 , 1 ] ,
49- outputRange : [ '0deg' , '360deg' ] ,
50- } ) ,
51- [ spin ] ,
52- ) ;
39+ const animatedProgress = useSharedValue ( 0 ) ;
40+ const rotation = useSharedValue ( 0 ) ;
5341
5442 const { cx, cy, r, circumference } = useMemo ( ( ) => {
5543 const pad = strokeWidth / 2 ;
@@ -67,18 +55,58 @@ export const CircularProgressIndicator = ({
6755 ? undefined
6856 : Math . min ( 100 , Math . max ( 0 , progress ) ) / 100 ;
6957
58+ useEffect ( ( ) => {
59+ if ( fraction === undefined ) {
60+ animatedProgress . value = 0 ;
61+ return ;
62+ }
63+
64+ animatedProgress . value = withTiming ( fraction , {
65+ duration : PROGRESS_ANIMATION_DURATION_MS ,
66+ easing : Easing . out ( Easing . cubic ) ,
67+ } ) ;
68+ } , [ animatedProgress , fraction ] ) ;
69+
70+ useEffect ( ( ) => {
71+ if ( fraction !== undefined ) {
72+ cancelAnimation ( rotation ) ;
73+ rotation . value = 0 ;
74+ return ;
75+ }
76+
77+ rotation . value = withRepeat (
78+ withTiming ( 360 , {
79+ duration : SPIN_DURATION_MS ,
80+ easing : Easing . linear ,
81+ } ) ,
82+ - 1 ,
83+ false ,
84+ ) ;
85+
86+ return ( ) => {
87+ cancelAnimation ( rotation ) ;
88+ } ;
89+ } , [ fraction , rotation ] ) ;
90+
91+ const animatedCircleProps = useAnimatedProps ( ( ) => ( {
92+ strokeDashoffset : circumference * ( 1 - animatedProgress . value ) ,
93+ } ) ) ;
94+
95+ const animatedSpinStyle = useAnimatedStyle < ViewStyle > ( ( ) => ( {
96+ transform : [ { rotate : `${ rotation . value } deg` } ] ,
97+ } ) ) ;
98+
7099 if ( fraction !== undefined ) {
71- const offset = circumference * ( 1 - fraction ) ;
72100 return (
73101 < Svg height = { size } style = { style } testID = { testID } viewBox = { `0 0 ${ size } ${ size } ` } width = { size } >
74- < Circle
102+ < AnimatedCircle
103+ animatedProps = { animatedCircleProps }
75104 cx = { cx }
76105 cy = { cy }
77106 fill = 'none'
78107 r = { r }
79108 stroke = { color as string }
80109 strokeDasharray = { `${ circumference } ` }
81- strokeDashoffset = { offset }
82110 strokeLinecap = 'round'
83111 strokeWidth = { strokeWidth }
84112 transform = { `rotate(-90 ${ cx } ${ cy } )` }
@@ -92,7 +120,7 @@ export const CircularProgressIndicator = ({
92120
93121 return (
94122 < Animated . View
95- style = { [ { height : size , width : size } , style , { transform : [ { rotate } ] } ] }
123+ style = { [ { height : size , width : size } , style , animatedSpinStyle ] }
96124 testID = { testID }
97125 >
98126 < Svg height = { size } viewBox = { `0 0 ${ size } ${ size } ` } width = { size } >
0 commit comments