UNPKG

6.44 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 = React.useRef<Animated.CompositeAnimation | null>(
79 null
80 );
81
82 const { scale } = theme.animation;
83
84 const startAnimation = React.useCallback(() => {
85 // Show progress bar
86 Animated.timing(fade, {
87 duration: 200 * scale,
88 toValue: 1,
89 useNativeDriver: true,
90 isInteraction: false,
91 }).start();
92
93 // Animate progress bar
94 if (indeterminate) {
95 if (!indeterminateAnimation.current) {
96 indeterminateAnimation.current = Animated.timing(timer, {
97 duration: INDETERMINATE_DURATION,
98 toValue: 1,
99 // Animated.loop does not work if useNativeDriver is true on web
100 useNativeDriver: Platform.OS !== 'web',
101 isInteraction: false,
102 });
103 }
104
105 // Reset timer to the beginning
106 timer.setValue(0);
107
108 Animated.loop(indeterminateAnimation.current).start();
109 } else {
110 Animated.timing(timer, {
111 duration: 200 * scale,
112 toValue: progress ? progress : 0,
113 useNativeDriver: true,
114 isInteraction: false,
115 }).start();
116 }
117 }, [scale, timer, progress, indeterminate, fade]);
118
119 const stopAnimation = React.useCallback(() => {
120 // Stop indeterminate animation
121 if (indeterminateAnimation.current) {
122 indeterminateAnimation.current.stop();
123 }
124
125 Animated.timing(fade, {
126 duration: 200 * scale,
127 toValue: 0,
128 useNativeDriver: true,
129 isInteraction: false,
130 }).start();
131 }, [fade, scale]);
132
133 React.useEffect(() => {
134 if (visible) startAnimation();
135 else stopAnimation();
136 }, [visible, startAnimation, stopAnimation]);
137
138 React.useEffect(() => {
139 // Start animation the very first time when previously the width was unclear
140 if (visible && prevWidth === 0) {
141 startAnimation();
142 }
143 }, [prevWidth, startAnimation, visible]);
144
145 const onLayout = (event: LayoutChangeEvent) => {
146 setPrevWidth(width);
147 setWidth(event.nativeEvent.layout.width);
148 };
149
150 const tintColor = color || theme.colors.primary;
151 const trackTintColor = setColor(tintColor).alpha(0.38).rgb().string();
152
153 return (
154 <View
155 onLayout={onLayout}
156 {...rest}
157 accessible
158 accessibilityRole="progressbar"
159 accessibilityState={{ busy: visible }}
160 accessibilityValue={
161 indeterminate ? {} : { min: 0, max: 100, now: progress * 100 }
162 }
163 >
164 <Animated.View
165 style={[
166 styles.container,
167 { backgroundColor: trackTintColor, opacity: fade },
168 style,
169 ]}
170 >
171 {width ? (
172 <Animated.View
173 style={[
174 styles.progressBar,
175 {
176 width,
177 backgroundColor: tintColor,
178 transform: [
179 {
180 translateX: timer.interpolate(
181 indeterminate
182 ? {
183 inputRange: [0, 0.5, 1],
184 outputRange: [
185 (isRTL ? 1 : -1) * 0.5 * width,
186 (isRTL ? 1 : -1) *
187 0.5 *
188 INDETERMINATE_MAX_WIDTH *
189 width,
190 (isRTL ? -1 : 1) * 0.7 * width,
191 ],
192 }
193 : {
194 inputRange: [0, 1],
195 outputRange: [(isRTL ? 1 : -1) * 0.5 * width, 0],
196 }
197 ),
198 },
199 {
200 // Workaround for workaround for https://github.com/facebook/react-native/issues/6278
201 scaleX: timer.interpolate(
202 indeterminate
203 ? {
204 inputRange: [0, 0.5, 1],
205 outputRange: [
206 0.0001,
207 INDETERMINATE_MAX_WIDTH,
208 0.0001,
209 ],
210 }
211 : {
212 inputRange: [0, 1],
213 outputRange: [0.0001, 1],
214 }
215 ),
216 },
217 ],
218 },
219 ]}
220 />
221 ) : null}
222 </Animated.View>
223 </View>
224 );
225};
226
227const styles = StyleSheet.create({
228 container: {
229 height: 4,
230 overflow: 'hidden',
231 },
232
233 progressBar: {
234 flex: 1,
235 },
236});
237
238export default withTheme(ProgressBar);