UNPKG

6.43 kBTypeScriptView Raw
1import * as React from 'react';
2import {
3 Animated,
4 Platform,
5 StyleSheet,
6 View,
7 ViewStyle,
8 StyleProp,
9 LayoutChangeEvent,
10 I18nManager,
11} from 'react-native';
12import setColor from 'color';
13import { withTheme } from '../core/theming';
14
15type Props = React.ComponentPropsWithRef<typeof View> & {
16 /**
17 * Progress value (between 0 and 1).
18 */
19 progress?: number;
20 /**
21 * Color of the progress bar. The background color will be calculated based on this but you can change it by passing `backgroundColor` to `style` prop.
22 */
23 color?: string;
24 /**
25 * If the progress bar will show indeterminate progress.
26 */
27 indeterminate?: boolean;
28 /**
29 * Whether to show the ProgressBar (true, the default) or hide it (false).
30 */
31 visible?: boolean;
32 style?: StyleProp<ViewStyle>;
33 /**
34 * @optional
35 */
36 theme: ReactNativePaper.Theme;
37};
38
39const INDETERMINATE_DURATION = 2000;
40const INDETERMINATE_MAX_WIDTH = 0.6;
41const { isRTL } = I18nManager;
42
43/**
44 * Progress bar is an indicator used to present progress of some activity in the app.
45 *
46 * <div class="screenshots">
47 * <img src="screenshots/progress-bar.png" />
48 * </div>
49 *
50 * ## Usage
51 * ```js
52 * import * as React from 'react';
53 * import { ProgressBar, Colors } from 'react-native-paper';
54 *
55 * const MyComponent = () => (
56 * <ProgressBar progress={0.5} color={Colors.red800} />
57 * );
58 *
59 * export default MyComponent;
60 * ```
61 */
62const ProgressBar = ({
63 color,
64 indeterminate,
65 style,
66 progress = 0,
67 visible = true,
68 theme,
69 ...rest
70}: Props) => {
71 const { current: timer } = React.useRef<Animated.Value>(
72 new Animated.Value(0)
73 );
74 const { current: fade } = React.useRef<Animated.Value>(new Animated.Value(0));
75 const [width, setWidth] = React.useState<number>(0);
76 const [prevWidth, setPrevWidth] = React.useState<number>(0);
77
78 const indeterminateAnimation =
79 React.useRef<Animated.CompositeAnimation | null>(null);
80
81 const { scale } = theme.animation;
82
83 const startAnimation = React.useCallback(() => {
84 // Show progress bar
85 Animated.timing(fade, {
86 duration: 200 * scale,
87 toValue: 1,
88 useNativeDriver: true,
89 isInteraction: false,
90 }).start();
91
92 // Animate progress bar
93 if (indeterminate) {
94 if (!indeterminateAnimation.current) {
95 indeterminateAnimation.current = Animated.timing(timer, {
96 duration: INDETERMINATE_DURATION,
97 toValue: 1,
98 // Animated.loop does not work if useNativeDriver is true on web
99 useNativeDriver: Platform.OS !== 'web',
100 isInteraction: false,
101 });
102 }
103
104 // Reset timer to the beginning
105 timer.setValue(0);
106
107 Animated.loop(indeterminateAnimation.current).start();
108 } else {
109 Animated.timing(timer, {
110 duration: 200 * scale,
111 toValue: progress ? progress : 0,
112 useNativeDriver: true,
113 isInteraction: false,
114 }).start();
115 }
116 }, [scale, timer, progress, indeterminate, fade]);
117
118 const stopAnimation = React.useCallback(() => {
119 // Stop indeterminate animation
120 if (indeterminateAnimation.current) {
121 indeterminateAnimation.current.stop();
122 }
123
124 Animated.timing(fade, {
125 duration: 200 * scale,
126 toValue: 0,
127 useNativeDriver: true,
128 isInteraction: false,
129 }).start();
130 }, [fade, scale]);
131
132 React.useEffect(() => {
133 if (visible) startAnimation();
134 else stopAnimation();
135 }, [visible, startAnimation, stopAnimation]);
136
137 React.useEffect(() => {
138 // Start animation the very first time when previously the width was unclear
139 if (visible && prevWidth === 0) {
140 startAnimation();
141 }
142 }, [prevWidth, startAnimation, visible]);
143
144 const onLayout = (event: LayoutChangeEvent) => {
145 setPrevWidth(width);
146 setWidth(event.nativeEvent.layout.width);
147 };
148
149 const tintColor = color || theme.colors.primary;
150 const trackTintColor = setColor(tintColor).alpha(0.38).rgb().string();
151
152 return (
153 <View
154 onLayout={onLayout}
155 {...rest}
156 accessible
157 accessibilityRole="progressbar"
158 accessibilityState={{ busy: visible }}
159 accessibilityValue={
160 indeterminate ? {} : { min: 0, max: 100, now: progress * 100 }
161 }
162 >
163 <Animated.View
164 style={[
165 styles.container,
166 { backgroundColor: trackTintColor, opacity: fade },
167 style,
168 ]}
169 >
170 {width ? (
171 <Animated.View
172 style={[
173 styles.progressBar,
174 {
175 width,
176 backgroundColor: tintColor,
177 transform: [
178 {
179 translateX: timer.interpolate(
180 indeterminate
181 ? {
182 inputRange: [0, 0.5, 1],
183 outputRange: [
184 (isRTL ? 1 : -1) * 0.5 * width,
185 (isRTL ? 1 : -1) *
186 0.5 *
187 INDETERMINATE_MAX_WIDTH *
188 width,
189 (isRTL ? -1 : 1) * 0.7 * width,
190 ],
191 }
192 : {
193 inputRange: [0, 1],
194 outputRange: [(isRTL ? 1 : -1) * 0.5 * width, 0],
195 }
196 ),
197 },
198 {
199 // Workaround for workaround for https://github.com/facebook/react-native/issues/6278
200 scaleX: timer.interpolate(
201 indeterminate
202 ? {
203 inputRange: [0, 0.5, 1],
204 outputRange: [
205 0.0001,
206 INDETERMINATE_MAX_WIDTH,
207 0.0001,
208 ],
209 }
210 : {
211 inputRange: [0, 1],
212 outputRange: [0.0001, 1],
213 }
214 ),
215 },
216 ],
217 },
218 ]}
219 />
220 ) : null}
221 </Animated.View>
222 </View>
223 );
224};
225
226const styles = StyleSheet.create({
227 container: {
228 height: 4,
229 overflow: 'hidden',
230 },
231
232 progressBar: {
233 flex: 1,
234 },
235});
236
237export default withTheme(ProgressBar);