UNPKG

6.47 kBTypeScriptView Raw
1import * as React from 'react';
2import {
3 Animated,
4 Easing,
5 Platform,
6 StyleProp,
7 StyleSheet,
8 View,
9 ViewStyle,
10} from 'react-native';
11import { withTheme } from '../core/theming';
12
13type Props = React.ComponentPropsWithRef<typeof View> & {
14 /**
15 * Whether to show the indicator or hide it.
16 */
17 animating?: boolean;
18 /**
19 * The color of the spinner.
20 */
21 color?: string;
22 /**
23 * Size of the indicator.
24 */
25 size?: 'small' | 'large' | number;
26 /**
27 * Whether the indicator should hide when not animating.
28 */
29 hidesWhenStopped?: boolean;
30 style?: StyleProp<ViewStyle>;
31 /**
32 * @optional
33 */
34 theme: ReactNativePaper.Theme;
35};
36
37const DURATION = 2400;
38
39/**
40 * Activity indicator is used to present progress of some activity in the app.
41 * It can be used as a drop-in for the ActivityIndicator shipped with React Native.
42 *
43 * <div class="screenshots">
44 * <img src="screenshots/activity-indicator.gif" style="width: 100px;" />
45 * </div>
46 *
47 * ## Usage
48 * ```js
49 * import * as React from 'react';
50 * import { ActivityIndicator, Colors } from 'react-native-paper';
51 *
52 * const MyComponent = () => (
53 * <ActivityIndicator animating={true} color={Colors.red800} />
54 * );
55 *
56 * export default MyComponent;
57 * ```
58 */
59const ActivityIndicator = ({
60 animating = true,
61 color: indicatorColor,
62 hidesWhenStopped = true,
63 size: indicatorSize = 'small',
64 style,
65 theme,
66 ...rest
67}: Props) => {
68 const { current: timer } = React.useRef<Animated.Value>(
69 new Animated.Value(0)
70 );
71 const { current: fade } = React.useRef<Animated.Value>(
72 new Animated.Value(!animating && hidesWhenStopped ? 0 : 1)
73 );
74
75 const rotation = React.useRef<Animated.CompositeAnimation | undefined>(
76 undefined
77 );
78
79 const {
80 animation: { scale },
81 } = theme;
82
83 const startRotation = React.useCallback(() => {
84 // Show indicator
85 Animated.timing(fade, {
86 duration: 200 * scale,
87 toValue: 1,
88 isInteraction: false,
89 useNativeDriver: true,
90 }).start();
91
92 // Circular animation in loop
93 if (rotation.current) {
94 timer.setValue(0);
95 // $FlowFixMe
96 Animated.loop(rotation.current).start();
97 }
98 }, [scale, fade, timer]);
99
100 const stopRotation = () => {
101 if (rotation.current) {
102 rotation.current.stop();
103 }
104 };
105
106 React.useEffect(() => {
107 if (rotation.current === undefined) {
108 // Circular animation in loop
109 rotation.current = Animated.timing(timer, {
110 duration: DURATION,
111 easing: Easing.linear,
112 // Animated.loop does not work if useNativeDriver is true on web
113 useNativeDriver: Platform.OS !== 'web',
114 toValue: 1,
115 isInteraction: false,
116 });
117 }
118
119 if (animating) {
120 startRotation();
121 } else if (hidesWhenStopped) {
122 // Hide indicator first and then stop rotation
123 Animated.timing(fade, {
124 duration: 200 * scale,
125 toValue: 0,
126 useNativeDriver: true,
127 isInteraction: false,
128 }).start(stopRotation);
129 } else {
130 stopRotation();
131 }
132 }, [animating, fade, hidesWhenStopped, startRotation, scale, timer]);
133
134 const color = indicatorColor || theme.colors.primary;
135 const size =
136 typeof indicatorSize === 'string'
137 ? indicatorSize === 'small'
138 ? 24
139 : 48
140 : indicatorSize
141 ? indicatorSize
142 : 24;
143
144 const frames = (60 * DURATION) / 1000;
145 const easing = Easing.bezier(0.4, 0.0, 0.7, 1.0);
146 const containerStyle = {
147 width: size,
148 height: size / 2,
149 overflow: 'hidden' as const,
150 };
151
152 return (
153 <View
154 style={[styles.container, style]}
155 {...rest}
156 accessible
157 accessibilityRole="progressbar"
158 accessibilityState={{ busy: animating }}
159 >
160 <Animated.View
161 style={[{ width: size, height: size, opacity: fade }]}
162 collapsable={false}
163 >
164 {[0, 1].map((index) => {
165 // Thanks to https://github.com/n4kz/react-native-indicators for the great work
166 const inputRange = Array.from(
167 new Array(frames),
168 (_, frameIndex) => frameIndex / (frames - 1)
169 );
170 const outputRange = Array.from(new Array(frames), (_, frameIndex) => {
171 let progress = (2 * frameIndex) / (frames - 1);
172 const rotation = index ? +(360 - 15) : -(180 - 15);
173
174 if (progress > 1.0) {
175 progress = 2.0 - progress;
176 }
177
178 const direction = index ? -1 : +1;
179
180 return `${direction * (180 - 30) * easing(progress) + rotation}deg`;
181 });
182
183 const layerStyle = {
184 width: size,
185 height: size,
186 transform: [
187 {
188 rotate: timer.interpolate({
189 inputRange: [0, 1],
190 outputRange: [`${0 + 30 + 15}deg`, `${2 * 360 + 30 + 15}deg`],
191 }),
192 },
193 ],
194 };
195
196 const viewportStyle = {
197 width: size,
198 height: size,
199 transform: [
200 {
201 translateY: index ? -size / 2 : 0,
202 },
203 {
204 rotate: timer.interpolate({ inputRange, outputRange }),
205 },
206 ],
207 };
208
209 const offsetStyle = index ? { top: size / 2 } : null;
210
211 const lineStyle = {
212 width: size,
213 height: size,
214 borderColor: color,
215 borderWidth: size / 10,
216 borderRadius: size / 2,
217 };
218
219 return (
220 <Animated.View key={index} style={[styles.layer]}>
221 <Animated.View style={layerStyle}>
222 <Animated.View
223 style={[containerStyle, offsetStyle]}
224 collapsable={false}
225 >
226 <Animated.View style={viewportStyle}>
227 <Animated.View style={containerStyle} collapsable={false}>
228 <Animated.View style={lineStyle} />
229 </Animated.View>
230 </Animated.View>
231 </Animated.View>
232 </Animated.View>
233 </Animated.View>
234 );
235 })}
236 </Animated.View>
237 </View>
238 );
239};
240
241const styles = StyleSheet.create({
242 container: {
243 justifyContent: 'center',
244 alignItems: 'center',
245 },
246
247 layer: {
248 ...StyleSheet.absoluteFillObject,
249
250 justifyContent: 'center',
251 alignItems: 'center',
252 },
253});
254
255export default withTheme(ActivityIndicator);