@baby-journey/rn-segmented-progress-bar
Version:
Animated circular progress bar with segments
203 lines (199 loc) • 9 kB
JavaScript
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
import React, { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef } from 'react';
import { Animated, Easing, StyleSheet, View } from 'react-native';
import Svg, { Circle, G, TSpan } from 'react-native-svg';
import { getArcEndCoordinates, getPathValues } from './helpers';
const IndicatorCircle = Animated.createAnimatedComponent(Circle);
const ProgressCircle = Animated.createAnimatedComponent(Circle);
const max = 100;
const duration = 1200;
const RNSegmentedProgressBar = (props, ref) => {
const {
radius,
strokeWidth = 10,
baseColor = '#ffede1',
progressColor = '#F39E93',
segments = 3,
segmentsGap = 0,
indicator,
centerComponent
} = props;
const circleRef = useRef([]);
const animatedValue = useRef(new Animated.Value(0)).current;
const progressAnimatedValues = useRef([...Array(segments)].map(() => new Animated.Value(0))).current;
const indicatorCircleRef = useRef(null);
const tSpanRef = useRef(null);
const indicatorSegmentsGap = (indicator === null || indicator === void 0 ? void 0 : indicator.radius) ?? 0;
const halfCircle = radius + strokeWidth + indicatorSegmentsGap;
const circleCircumference = 2 * Math.PI * radius;
const rotation = -90 + 180 * (segmentsGap / 2 / radius) / Math.PI;
const getProgressValues = useCallback(progress => getPathValues(progress, max, segments), [segments]);
const progressDelay = 10;
const animation = useCallback((animatedVal, toValue, delay, durationValue) => {
return Animated.timing(animatedVal, {
toValue,
duration: durationValue,
delay,
useNativeDriver: true,
easing: Easing.linear
});
}, []);
useEffect(() => {
() => {
animatedValue.removeAllListeners();
progressAnimatedValues.forEach(progressAnimatedValue => progressAnimatedValue.removeAllListeners());
};
}, [animatedValue, progressAnimatedValues]);
const getMeanSegmentsGap = useCallback(progress => {
const pathValues = getProgressValues(progress);
return (progress / pathValues.filter(val => val > 0).length || 1) * segments * segmentsGap / 100;
}, [getProgressValues, segments, segmentsGap]);
const runIndicator = useCallback((calculatedStrokeDashoffset, val) => {
const {
x: cx,
y: cy
} = getArcEndCoordinates(radius, calculatedStrokeDashoffset, halfCircle, halfCircle, rotation);
if (!calculatedStrokeDashoffset) {
return;
}
const calculatedProgress = `${Math.round(val)}%`;
if (indicatorCircleRef !== null && indicatorCircleRef !== void 0 && indicatorCircleRef.current && tSpanRef !== null && tSpanRef !== void 0 && tSpanRef.current) {
//@ts-ignore
indicatorCircleRef.current.setNativeProps({
r: (indicator === null || indicator === void 0 ? void 0 : indicator.radius) || 0,
strokeWidth: (indicator === null || indicator === void 0 ? void 0 : indicator.strokeWidth) || 0,
cx,
cy
});
//@ts-ignore
tSpanRef === null || tSpanRef === void 0 ? void 0 : tSpanRef.current.setNativeProps({
children: calculatedProgress,
dx: cx,
dy: cy + 5,
font: {
textAnchor: 'middle',
fontSize: 18
}
});
}
}, [radius, halfCircle, rotation, indicator === null || indicator === void 0 ? void 0 : indicator.radius, indicator === null || indicator === void 0 ? void 0 : indicator.strokeWidth]);
const run = useCallback(_ref => {
let {
progress
} = _ref;
const circleProgressValues = getProgressValues(progress);
progressAnimatedValues.forEach((progressAnimated, index) => {
progressAnimated.addListener(v => {
if (circleRef !== null && circleRef !== void 0 && circleRef.current[index]) {
var _circleRef$current$in;
var strokeDashoffset = circleCircumference;
var val = v.value <= (circleProgressValues[index] ?? 0) ? v.value : circleProgressValues[index] ?? 0;
strokeDashoffset = circleProgressValues[index] ? circleCircumference - circleCircumference * val / 100 : circleCircumference;
const paintedLength = circleCircumference - strokeDashoffset - segments * (circleProgressValues[index] ?? 0) * segmentsGap / 100;
//@ts-ignore
circleRef === null || circleRef === void 0 ? void 0 : (_circleRef$current$in = circleRef.current[index]) === null || _circleRef$current$in === void 0 ? void 0 : _circleRef$current$in.setNativeProps({
strokeDashoffset: circleCircumference - paintedLength > circleCircumference ? circleCircumference : circleCircumference - paintedLength
});
}
});
});
if (indicator !== null && indicator !== void 0 && indicator.show) {
animatedValue.addListener(v => {
var strokeDashoffset = circleCircumference;
var val = v.value <= progress ? v.value : progress;
strokeDashoffset = progress ? circleCircumference - circleCircumference * val / 100 : circleCircumference;
const paintedLength = circleCircumference - strokeDashoffset;
const meanSegmentsGap = getMeanSegmentsGap(progress);
const calculatedStrokeDashoffset = paintedLength - meanSegmentsGap;
runIndicator(calculatedStrokeDashoffset, progress);
});
}
// Animate circles sequentially
const progressAnimations = Animated.sequence(progressAnimatedValues.map((tav, index) => animation(tav,
// Animated value
circleProgressValues[index] ?? 0,
// To value
index === 0 ? progressDelay : 0,
// Delay
duration * (circleProgressValues[index] ?? 0) / max // Duration
)));
if (indicator !== null && indicator !== void 0 && indicator.show) {
// Animate percentage circle
const percentageAnim = animation(animatedValue,
// Animated value
progress,
// To value
progressDelay,
// Delay
duration * progress / max // Duration
);
// Progress Animations run parallelly with percentage circle
Animated.parallel([progressAnimations, percentageAnim]).start();
} else {
progressAnimations.start();
}
}, [animatedValue, animation, segments, circleCircumference, segmentsGap, getMeanSegmentsGap, indicator === null || indicator === void 0 ? void 0 : indicator.show, getProgressValues, runIndicator, progressAnimatedValues]);
const getProgress = useMemo(() => {
const progressConfig = {
stroke: progressColor,
cx: halfCircle,
cy: halfCircle,
r: radius,
origin: `${halfCircle}, ${halfCircle}`,
strokeWidth: strokeWidth,
strokeDasharray: circleCircumference,
strokeDashoffset: circleCircumference
};
return progressAnimatedValues.map((_, key) => /*#__PURE__*/React.createElement(ProgressCircle, _extends({
key: key
//@ts-ignore
,
ref: el => circleRef.current[key] = el
}, progressConfig, {
rotation: rotation + key * 360 / segments,
strokeLinecap: "round"
})));
}, [segments, circleCircumference, halfCircle, progressColor, radius, rotation, strokeWidth, progressAnimatedValues]);
useImperativeHandle(ref, () => ({
run
}));
return /*#__PURE__*/React.createElement(Svg, {
viewBox: `0 0 ${halfCircle * 2} ${halfCircle * 2}`,
width: '100%',
fill: "none",
height: radius * 2
}, centerComponent && /*#__PURE__*/React.createElement(View, {
style: styles.centerComponent
}, centerComponent), /*#__PURE__*/React.createElement(G, null, [...Array(segments)].map((_, key) => {
return /*#__PURE__*/React.createElement(Circle, {
key: key,
cx: halfCircle,
cy: halfCircle,
r: radius,
stroke: baseColor,
rotation: rotation + key * 360 / segments,
origin: `${halfCircle}, ${halfCircle}`,
strokeWidth: strokeWidth,
strokeDasharray: circleCircumference,
strokeDashoffset: circleCircumference - circleCircumference / segments + segmentsGap,
strokeLinecap: "round"
});
}), getProgress, (indicator === null || indicator === void 0 ? void 0 : indicator.show) === true && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(IndicatorCircle, {
stroke: progressColor,
ref: indicatorCircleRef,
fill: "white"
}), /*#__PURE__*/React.createElement(TSpan, {
stroke: progressColor,
fill: progressColor,
ref: tSpanRef
}))));
};
export default /*#__PURE__*/memo( /*#__PURE__*/forwardRef(RNSegmentedProgressBar));
const styles = StyleSheet.create({
centerComponent: {
height: '100%',
justifyContent: 'center',
alignItems: 'center'
}
});
//# sourceMappingURL=index.js.map