@shoutem/ui 8.0.0-rc.0 → 8.1.0-rc.0
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/components/ProgressRings/AnimatedProgressRing.js +30 -0
- package/components/ProgressRings/ProgressRing.js +85 -0
- package/components/ProgressRings/ProgressRings.js +68 -0
- package/components/ProgressRings/const.js +20 -0
- package/components/ProgressRings/index.js +4 -0
- package/components/ProgressRings/svgCalculations.js +26 -0
- package/hooks/index.js +1 -0
- package/hooks/useColorInterpolation.js +60 -0
- package/index.js +2 -0
- package/package.json +1 -1
- package/theme.js +7 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/* eslint-disable react/require-default-props */
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import _ from 'lodash';
|
|
4
|
+
import PropTypes from 'prop-types';
|
|
5
|
+
import { useColorInterpolation } 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
|
+
percentage = PROGRESS_RING_DEFAULT_PROPS.percentage,
|
|
12
|
+
progressColors = DEFAULT_PROGRESS_COLORS,
|
|
13
|
+
} = props;
|
|
14
|
+
|
|
15
|
+
const interpolatedColor = useColorInterpolation(progressColors, percentage);
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<ProgressRing
|
|
19
|
+
{..._.omit(props, ['progressColors'])}
|
|
20
|
+
color={interpolatedColor}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
AnimatedProgressRing.propTypes = {
|
|
26
|
+
..._.omit(ProgressRing.propTypes, ['color']),
|
|
27
|
+
progressColors: PropTypes.arrayOf(PropTypes.string),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default AnimatedProgressRing;
|
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
width = PROGRESS_RING_DEFAULT_PROPS.width,
|
|
12
|
+
backgroundWidth = PROGRESS_RING_DEFAULT_PROPS.backgroundWidth,
|
|
13
|
+
progressLineCap = PROGRESS_RING_DEFAULT_PROPS.progressLineCap,
|
|
14
|
+
backgroundLineCap = PROGRESS_RING_DEFAULT_PROPS.backgroundLineCap,
|
|
15
|
+
percentage = PROGRESS_RING_DEFAULT_PROPS.percentage,
|
|
16
|
+
hasBackgroundColor = PROGRESS_RING_DEFAULT_PROPS.hasBackgroundColor,
|
|
17
|
+
arcSweepAngle = PROGRESS_RING_DEFAULT_PROPS.arcSweepAngle,
|
|
18
|
+
size = PROGRESS_RING_DEFAULT_PROPS.size,
|
|
19
|
+
color = PROGRESS_RING_DEFAULT_PROPS.color,
|
|
20
|
+
}) => {
|
|
21
|
+
const halfSize = size / 2;
|
|
22
|
+
|
|
23
|
+
const maxWidthCircle = backgroundWidth
|
|
24
|
+
? Math.max(width, backgroundWidth)
|
|
25
|
+
: width;
|
|
26
|
+
|
|
27
|
+
const radius = halfSize - maxWidthCircle / 2 - index * maxWidthCircle;
|
|
28
|
+
|
|
29
|
+
const currentFillAngle =
|
|
30
|
+
(arcSweepAngle * Math.min(100, Math.max(0, percentage))) / 100;
|
|
31
|
+
|
|
32
|
+
const backgroundPath = circlePath(
|
|
33
|
+
halfSize,
|
|
34
|
+
halfSize,
|
|
35
|
+
radius,
|
|
36
|
+
currentFillAngle,
|
|
37
|
+
arcSweepAngle,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const progressPath = circlePath(
|
|
41
|
+
halfSize,
|
|
42
|
+
halfSize,
|
|
43
|
+
radius,
|
|
44
|
+
0,
|
|
45
|
+
currentFillAngle,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<>
|
|
50
|
+
{hasBackgroundColor && (
|
|
51
|
+
<Path
|
|
52
|
+
d={backgroundPath}
|
|
53
|
+
stroke={changeColorAlpha(color, 0.2)}
|
|
54
|
+
strokeWidth={backgroundWidth || width}
|
|
55
|
+
strokeLinecap={backgroundLineCap}
|
|
56
|
+
fill="transparent"
|
|
57
|
+
/>
|
|
58
|
+
)}
|
|
59
|
+
{percentage > 0 && (
|
|
60
|
+
<Path
|
|
61
|
+
d={progressPath}
|
|
62
|
+
stroke={color}
|
|
63
|
+
strokeWidth={width}
|
|
64
|
+
strokeLinecap={progressLineCap}
|
|
65
|
+
fill="transparent"
|
|
66
|
+
/>
|
|
67
|
+
)}
|
|
68
|
+
</>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
ProgressRing.propTypes = {
|
|
73
|
+
index: PropTypes.number.isRequired,
|
|
74
|
+
arcSweepAngle: PropTypes.number,
|
|
75
|
+
backgroundLineCap: PropTypes.string,
|
|
76
|
+
backgroundWidth: PropTypes.number,
|
|
77
|
+
color: PropTypes.string,
|
|
78
|
+
hasBackgroundColor: PropTypes.bool,
|
|
79
|
+
percentage: PropTypes.number,
|
|
80
|
+
progressLineCap: PropTypes.string,
|
|
81
|
+
size: PropTypes.number,
|
|
82
|
+
width: PropTypes.number,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
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,20 @@
|
|
|
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
|
+
width: 10,
|
|
12
|
+
backgroundWidth: 10,
|
|
13
|
+
progressLineCap: 'round',
|
|
14
|
+
backgroundLineCap: 'round',
|
|
15
|
+
percentage: 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
|
+
hasBackgroundColor: true,
|
|
19
|
+
color: '#000',
|
|
20
|
+
};
|
|
@@ -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 @@
|
|
|
1
|
+
export { useColorInterpolation } from './useColorInterpolation';
|
|
@@ -0,0 +1,60 @@
|
|
|
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, percentage) => {
|
|
33
|
+
const animatedPercentage = useRef(new Animated.Value(percentage)).current;
|
|
34
|
+
|
|
35
|
+
const [interpolatedColor, setInterpolatedColor] = useState(colors[0]);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
Animated.timing(animatedPercentage, {
|
|
39
|
+
toValue: percentage,
|
|
40
|
+
duration: 100,
|
|
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
|
+
}, [percentage, colors, animatedPercentage]);
|
|
58
|
+
|
|
59
|
+
return interpolatedColor;
|
|
60
|
+
};
|
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
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
|
};
|