@shoutem/ui 8.0.0 → 8.1.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -30,7 +30,7 @@ Join [our community](https://www.facebook.com/groups/shoutem.community/) on Face
30
30
 
31
31
  ## UI Toolkit
32
32
 
33
- Shoutem UI is a part of the [Shoutem UI Toolkit](https://shoutem.github.io/ui/) that enables you to build professional looking React Native apps with ease.
33
+ Shoutem UI is a part of the [Shoutem UI Toolkit](https://shoutem.github.io/docs/ui-toolkit/introduction) that enables you to build professional looking React Native apps with ease.
34
34
 
35
35
  It consists of three libraries:
36
36
 
@@ -0,0 +1,34 @@
1
+ /* eslint-disable react/require-default-props */
2
+ import React from 'react';
3
+ import _ from 'lodash';
4
+ import PropTypes from 'prop-types';
5
+ import { useColorAndPercentageInterpolation } from '../../hooks';
6
+ import { DEFAULT_PROGRESS_COLORS, PROGRESS_RING_DEFAULT_PROPS } from './const';
7
+ import ProgressRing from './ProgressRing';
8
+
9
+ const AnimatedProgressRing = props => {
10
+ const {
11
+ progressPercentage = PROGRESS_RING_DEFAULT_PROPS.progressPercentage,
12
+ progressColors = DEFAULT_PROGRESS_COLORS,
13
+ } = props;
14
+
15
+ const {
16
+ interpolatedColor,
17
+ interpolatedPercentage,
18
+ } = useColorAndPercentageInterpolation(progressColors, progressPercentage);
19
+
20
+ return (
21
+ <ProgressRing
22
+ {..._.omit(props, ['progressColors'])}
23
+ color={interpolatedColor}
24
+ progressPercentage={interpolatedPercentage}
25
+ />
26
+ );
27
+ };
28
+
29
+ AnimatedProgressRing.propTypes = {
30
+ ..._.omit(ProgressRing.propTypes, ['color']),
31
+ progressColors: PropTypes.arrayOf(PropTypes.string),
32
+ };
33
+
34
+ export default AnimatedProgressRing;
@@ -0,0 +1,83 @@
1
+ /* eslint-disable react/require-default-props */
2
+ import React from 'react';
3
+ import { Path } from 'react-native-svg';
4
+ import PropTypes from 'prop-types';
5
+ import { changeColorAlpha } from '@shoutem/theme';
6
+ import { PROGRESS_RING_DEFAULT_PROPS } from './const';
7
+ import { circlePath } from './svgCalculations';
8
+
9
+ const ProgressRing = ({
10
+ index,
11
+ progressPercentage = PROGRESS_RING_DEFAULT_PROPS.progressPercentage,
12
+ size = PROGRESS_RING_DEFAULT_PROPS.size,
13
+ color = PROGRESS_RING_DEFAULT_PROPS.color,
14
+ progressLineWidth = PROGRESS_RING_DEFAULT_PROPS.progressLineWidth,
15
+ backgroundLineWidth = PROGRESS_RING_DEFAULT_PROPS.backgroundLineWidth,
16
+ progressLineCap = PROGRESS_RING_DEFAULT_PROPS.progressLineCap,
17
+ backgroundLineCap = PROGRESS_RING_DEFAULT_PROPS.backgroundLineCap,
18
+ arcSweepAngle = PROGRESS_RING_DEFAULT_PROPS.arcSweepAngle,
19
+ }) => {
20
+ const halfSize = size / 2;
21
+
22
+ const maxWidthCircle = backgroundLineWidth
23
+ ? Math.max(progressLineWidth, backgroundLineWidth)
24
+ : progressLineWidth;
25
+
26
+ const radius = halfSize - maxWidthCircle / 2 - index * maxWidthCircle;
27
+
28
+ const currentFillAngle =
29
+ (arcSweepAngle * Math.min(100, Math.max(0, progressPercentage))) / 100;
30
+
31
+ const backgroundPath = circlePath(
32
+ halfSize,
33
+ halfSize,
34
+ radius,
35
+ currentFillAngle,
36
+ arcSweepAngle,
37
+ );
38
+
39
+ const progressPath = circlePath(
40
+ halfSize,
41
+ halfSize,
42
+ radius,
43
+ 0,
44
+ currentFillAngle,
45
+ );
46
+
47
+ return (
48
+ <>
49
+ {backgroundLineWidth > 0 && (
50
+ <Path
51
+ d={backgroundPath}
52
+ stroke={changeColorAlpha(color, 0.2)}
53
+ strokeWidth={backgroundLineWidth || progressLineWidth}
54
+ strokeLinecap={backgroundLineCap}
55
+ fill="transparent"
56
+ />
57
+ )}
58
+ {progressPercentage > 0 && (
59
+ <Path
60
+ d={progressPath}
61
+ stroke={color}
62
+ strokeWidth={progressLineWidth}
63
+ strokeLinecap={progressLineCap}
64
+ fill="transparent"
65
+ />
66
+ )}
67
+ </>
68
+ );
69
+ };
70
+
71
+ ProgressRing.propTypes = {
72
+ index: PropTypes.number.isRequired,
73
+ arcSweepAngle: PropTypes.number,
74
+ backgroundLineCap: PropTypes.string,
75
+ backgroundLineWidth: PropTypes.number,
76
+ color: PropTypes.string,
77
+ progressLineCap: PropTypes.string,
78
+ progressLineWidth: PropTypes.number,
79
+ progressPercentage: PropTypes.number,
80
+ size: PropTypes.number,
81
+ };
82
+
83
+ export default ProgressRing;
@@ -0,0 +1,68 @@
1
+ import React, { useMemo } from 'react';
2
+ import { View } from 'react-native';
3
+ import { G, Svg } from 'react-native-svg';
4
+ import _ from 'lodash';
5
+ import PropTypes from 'prop-types';
6
+ import { connectStyle } from '@shoutem/theme';
7
+ import AnimatedProgressRing from './AnimatedProgressRing';
8
+ import ProgressRing from './ProgressRing';
9
+
10
+ /**
11
+ * Component rendering given number of rings, which are filled depending on given
12
+ * percentage values.
13
+ * It is possible to either specify definite color for each ring by passing ring.color prop,
14
+ * or give and array of ring.progressColors and component will then interpolate between given
15
+ * colors, based on given percentage value.
16
+ */
17
+ const ProgressRings = ({ rings, size, rotation, children, style }) => {
18
+ const renderRings = useMemo(
19
+ () =>
20
+ rings.map((ring, index) => {
21
+ const ResolvedRingComponent = ring.progressColors
22
+ ? AnimatedProgressRing
23
+ : ProgressRing;
24
+
25
+ return (
26
+ <ResolvedRingComponent
27
+ key={ring.id}
28
+ index={index}
29
+ size={size}
30
+ {...ring}
31
+ />
32
+ );
33
+ }),
34
+ [rings, size],
35
+ );
36
+
37
+ return (
38
+ <View style={style.container}>
39
+ <Svg width={size} height={size}>
40
+ <G rotation={rotation} originX={size / 2} originY={size / 2}>
41
+ {renderRings}
42
+ </G>
43
+ </Svg>
44
+ {children && <View style={style.childrenContainer}>{children}</View>}
45
+ </View>
46
+ );
47
+ };
48
+
49
+ ProgressRings.propTypes = {
50
+ rings: PropTypes.arrayOf(
51
+ // Omitting color to stop errors, but implementation has to define either color or progressColors,
52
+ // depending on which component they're using - ProgressRing or AnimatedProgressRing, respectively.
53
+ PropTypes.shape({ ..._.omit(ProgressRing.propTypes, ['index', 'color']) }),
54
+ ).isRequired,
55
+ children: PropTypes.func,
56
+ rotation: PropTypes.number,
57
+ size: PropTypes.number,
58
+ style: PropTypes.object,
59
+ };
60
+
61
+ ProgressRings.defaultProps = {
62
+ children: undefined,
63
+ size: 80,
64
+ rotation: 0,
65
+ style: {},
66
+ };
67
+
68
+ export default connectStyle('shoutem.ui.ProgressRings')(ProgressRings);
@@ -0,0 +1,19 @@
1
+ export const DEFAULT_PROGRESS_COLORS = [
2
+ '#FF0000', // Red
3
+ '#FF4500', // Orange-Red
4
+ '#FFA500', // Orange
5
+ '#FFD700', // Yellow
6
+ '#ADFF2F', // Yellow-Green
7
+ '#90EE90', // Light Green
8
+ ];
9
+
10
+ export const PROGRESS_RING_DEFAULT_PROPS = {
11
+ progressLineWidth: 10,
12
+ backgroundLineWidth: 10,
13
+ progressLineCap: 'round',
14
+ backgroundLineCap: 'round',
15
+ progressPercentage: 0.1, // 0.1 so that it shows tiny fill indicator, indicating 0%, better UX/UI than empty.
16
+ arcSweepAngle: 360,
17
+ size: 80,
18
+ color: '#000',
19
+ };
@@ -0,0 +1,4 @@
1
+ export { default as AnimatedProgressRing } from './AnimatedProgressRing';
2
+ export * from './const';
3
+ export { default as ProgressRing } from './ProgressRing';
4
+ export { default as ProgressRings } from './ProgressRings';
@@ -0,0 +1,26 @@
1
+ const polarToCartesian = (centerX, centerY, radius, angleInDegrees) => {
2
+ const angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180.0;
3
+ return {
4
+ x: centerX + radius * Math.cos(angleInRadians),
5
+ y: centerY + radius * Math.sin(angleInRadians),
6
+ };
7
+ };
8
+
9
+ export const circlePath = (x, y, radius, startAngle, endAngle) => {
10
+ const start = polarToCartesian(x, y, radius, endAngle * 0.9999999);
11
+ const end = polarToCartesian(x, y, radius, startAngle);
12
+ const largeArcFlag = endAngle - startAngle <= 180 ? '0' : '1';
13
+ return [
14
+ 'M',
15
+ start.x,
16
+ start.y,
17
+ 'A',
18
+ radius,
19
+ radius,
20
+ 0,
21
+ largeArcFlag,
22
+ 0,
23
+ end.x,
24
+ end.y,
25
+ ].join(' ');
26
+ };
package/hooks/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export {
2
+ useColorAndPercentageInterpolation,
3
+ useColorInterpolation,
4
+ } from './useColorAndPercentageInterpolation';
@@ -0,0 +1,95 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { Animated } from 'react-native';
3
+
4
+ const resolveInputRange = colors => {
5
+ if (colors.length === 1) {
6
+ // If only one color is given, color will always be the same, no matter of percentage.
7
+ return [0, 100];
8
+ }
9
+
10
+ const numSteps = colors.length - 1;
11
+ const stepSize = 100 / numSteps;
12
+
13
+ return colors.map((_, index) => Math.round(index * stepSize));
14
+ };
15
+
16
+ const resolveOutputRange = colors => {
17
+ if (colors.length === 1) {
18
+ // If only one color is given, color will always be the same, no matter of percentage.
19
+ return [colors[0], colors[0]];
20
+ }
21
+
22
+ return colors;
23
+ };
24
+
25
+ const resolveInterpolationRange = colors => {
26
+ return {
27
+ inputRange: resolveInputRange(colors),
28
+ outputRange: resolveOutputRange(colors),
29
+ };
30
+ };
31
+
32
+ export const useColorInterpolation = (colors, progressPerecentage) => {
33
+ const animatedPercentage = useRef(new Animated.Value(0)).current;
34
+
35
+ const [interpolatedColor, setInterpolatedColor] = useState(colors[0]);
36
+
37
+ useEffect(() => {
38
+ Animated.timing(animatedPercentage, {
39
+ toValue: progressPerecentage,
40
+ duration: 1000,
41
+ useNativeDriver: false, // Color interpolation requires native driver to be false
42
+ }).start();
43
+
44
+ // Listen to animatedPercentage value changes and update the interpolated color
45
+ const listener = animatedPercentage.addListener(() => {
46
+ const colorInterpolation = animatedPercentage.interpolate(
47
+ resolveInterpolationRange(colors),
48
+ );
49
+
50
+ // Resolve the interpolated color to a valid color string
51
+ setInterpolatedColor(colorInterpolation.__getValue());
52
+ });
53
+
54
+ return () => {
55
+ animatedPercentage.removeListener(listener);
56
+ };
57
+ }, [progressPerecentage, colors, animatedPercentage]);
58
+
59
+ return interpolatedColor;
60
+ };
61
+
62
+ export const useColorAndPercentageInterpolation = (
63
+ colors,
64
+ progressPerecentage,
65
+ ) => {
66
+ const animatedPercentage = useRef(new Animated.Value(0)).current;
67
+
68
+ const [interpolatedColor, setInterpolatedColor] = useState(colors[0]);
69
+ const [interpolatedPercentage, setInterpolatedPercentage] = useState(0);
70
+
71
+ useEffect(() => {
72
+ Animated.timing(animatedPercentage, {
73
+ toValue: progressPerecentage,
74
+ duration: 1000,
75
+ useNativeDriver: false, // Color interpolation requires native driver to be false
76
+ }).start();
77
+
78
+ // Listen to animatedPercentage value changes and update the interpolated color
79
+ const listener = animatedPercentage.addListener(({ value }) => {
80
+ const colorInterpolation = animatedPercentage.interpolate(
81
+ resolveInterpolationRange(colors),
82
+ );
83
+
84
+ // Resolve the interpolated color to a valid color string
85
+ setInterpolatedColor(colorInterpolation.__getValue());
86
+ setInterpolatedPercentage(value);
87
+ });
88
+
89
+ return () => {
90
+ animatedPercentage.removeListener(listener);
91
+ };
92
+ }, [progressPerecentage, colors, animatedPercentage]);
93
+
94
+ return { interpolatedColor, interpolatedPercentage };
95
+ };
package/index.js CHANGED
@@ -53,6 +53,7 @@ export { LoadingIndicator } from './components/LoadingIndicator';
53
53
  export { NumberInput } from './components/NumberInput';
54
54
  export { Overlay } from './components/Overlay';
55
55
  export { PageIndicators } from './components/PageIndicators';
56
+ export * from './components/ProgressRings';
56
57
  export { Row } from './components/Row';
57
58
  export { Screen } from './components/Screen';
58
59
  export { ScrollView } from './components/ScrollView';
@@ -71,6 +72,7 @@ export { TouchableOpacity } from './components/TouchableOpacity';
71
72
  export { Video } from './components/Video';
72
73
  export { View } from './components/View';
73
74
  export { YearRangePicker } from './components/YearRangePicker';
75
+ export * from './hooks';
74
76
 
75
77
  // Helpers
76
78
  export { calculateKeyboardOffset, Device, Keyboard } from './helpers';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shoutem/ui",
3
- "version": "8.0.0",
3
+ "version": "8.1.0-rc.1",
4
4
  "description": "Styleable set of components for React Native applications",
5
5
  "scripts": {
6
6
  "lint": "eslint .",
package/theme.js CHANGED
@@ -2918,5 +2918,12 @@ export default () => {
2918
2918
  maxHeight: responsiveHeight(20),
2919
2919
  },
2920
2920
  },
2921
+
2922
+ 'shoutem.ui.ProgressRings': {
2923
+ container: { justifyContent: 'center', alignItems: 'center' },
2924
+ childrenContainer: {
2925
+ position: 'absolute',
2926
+ },
2927
+ },
2921
2928
  };
2922
2929
  };