Skip to content

Commit 72aaece

Browse files
committed
feat(spinner): ✨ add linear gradient to spinner component
1 parent eca16e7 commit 72aaece

File tree

2 files changed

+126
-43
lines changed

2 files changed

+126
-43
lines changed

src/components/spinner/Spinner.tsx

Lines changed: 120 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
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";
1216
import { useTheme } from "../../theme";
13-
import { createComponent, cx, styleAdapter } from "../../utils";
17+
import { createComponent, styleAdapter } from "../../utils";
18+
19+
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
1420

1521
export type SpinnerSizes = "xs" | "sm" | "md" | "lg" | "xl";
1622
export 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-
4347
const 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

src/theme/defaultTheme/spinner.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
export const spinner = {
22
base: "border-solid rounded-full border-[1.5px]",
33
track: {
4-
transparent: "border-b-transparent border-l-transparent",
4+
transparent: "border-transparent",
55
visible: {
6-
base: "border-b-gray-400 border-l-gray-400",
7-
primary: "border-b-blue-400 border-l-blue-400",
8-
secondary: "border-b-violet-400 border-l-violet-400",
9-
success: "border-b-green-400 border-l-green-400",
10-
danger: "border-b-red-400 border-l-red-400",
6+
base: "border-gray-400",
7+
primary: "border-blue-400",
8+
secondary: "border-violet-400",
9+
success: "border-green-400",
10+
danger: "border-red-400",
1111
},
1212
},
1313
themeColor: {

0 commit comments

Comments
 (0)