UNPKG

@baby-journey/rn-segmented-progress-bar

Version:
268 lines (255 loc) 11.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _react = require("react"); var _reactNative = require("react-native"); var _reactNativeSvg = _interopRequireWildcard(require("react-native-svg")); var _helpers = require("./helpers"); var _jsxRuntime = require("react/jsx-runtime"); function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } const IndicatorCircle = _reactNative.Animated.createAnimatedComponent(_reactNativeSvg.Circle); const ProgressCircle = _reactNative.Animated.createAnimatedComponent(_reactNativeSvg.Circle); const max = 100; const duration = 1200; const progressDelay = 500; const INDICATOR_FONT = { textAnchor: 'middle', fontSize: 18 }; /** * Computes the declarative strokeDashoffset for a segment. * Returns an Animated.AnimatedInterpolation when the segment has progress, * or a static number when it doesn't. */ const computeStrokeDashoffset = (animatedVal, target, circumference, numSegments, gap) => { if (target <= 0) return circumference; const gapAdjust = numSegments * target * gap / 100; const targetOffset = circumference * (1 - target / 100) + gapAdjust; const thresholdV = gapAdjust > 0 ? gapAdjust * 100 / circumference : 0; // Gap consumes all progress — segment stays hidden if (thresholdV >= target) return circumference; // With gap: dead zone at start where offset stays at circumference if (thresholdV > 0) { return animatedVal.interpolate({ inputRange: [0, thresholdV, target], outputRange: [circumference, circumference, targetOffset], extrapolate: 'clamp' }); } // No gap: simple linear interpolation return animatedVal.interpolate({ inputRange: [0, target], outputRange: [circumference, targetOffset], extrapolate: 'clamp' }); }; const RNSegmentedProgressBar = (props, ref) => { const { radius, strokeWidth = 10, baseColor = '#ffede1', progressColor = '#F39E93', segments = 3, segmentsGap = 0, indicator, centerComponent } = props; const animatedValue = (0, _react.useRef)(new _reactNative.Animated.Value(0)).current; const progressAnimatedValues = (0, _react.useRef)(new Array(segments).fill(null).map(() => new _reactNative.Animated.Value(0))).current; const indicatorCircleRef = (0, _react.useRef)(null); const tSpanRef = (0, _react.useRef)(null); // Per-segment target values — drives both render-time interpolation and animation const [segmentTargets, setSegmentTargets] = (0, _react.useState)(() => new Array(segments).fill(0)); // Overall progress stored in ref (only needed inside animation, not for render) const activeProgressRef = (0, _react.useRef)(0); // Pending animation config — bridges run() and the post-render effect const pendingAnimationRef = (0, _react.useRef)(null); const indicatorSegmentsGap = 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 = (0, _react.useCallback)(progress => (0, _helpers.getPathValues)(progress, max, segments), [segments]); const getMeanSegmentsGap = (0, _react.useCallback)(progress => { const pathValues = getProgressValues(progress); const activeSegments = pathValues.filter(val => val > 0).length; return progress / (activeSegments || 1) * segments * segmentsGap / 100; }, [getProgressValues, segments, segmentsGap]); // Clean up on unmount (0, _react.useEffect)(() => { return () => { animatedValue.stopAnimation(); animatedValue.removeAllListeners(); progressAnimatedValues.forEach(v => { v.stopAnimation(); v.removeAllListeners(); }); }; }, [animatedValue, progressAnimatedValues]); // Start animations AFTER React has re-rendered with updated segmentTargets. // This guarantees progressTrack useMemo has correct interpolations before // any animated values start ticking. (0, _react.useEffect)(() => { const pending = pendingAnimationRef.current; if (!pending) return; pendingAnimationRef.current = null; const { progress, targets } = pending; const progressAnimations = _reactNative.Animated.sequence(progressAnimatedValues.map((animVal, index) => _reactNative.Animated.timing(animVal, { toValue: targets[index] ?? 0, duration: duration * (targets[index] ?? 0) / max, delay: index === 0 ? progressDelay : 0, useNativeDriver: false, easing: _reactNative.Easing.linear }))); if (indicator?.show) { const percentageAnim = _reactNative.Animated.timing(animatedValue, { toValue: progress, duration: duration * progress / max, delay: progressDelay, useNativeDriver: false, easing: _reactNative.Easing.linear }); _reactNative.Animated.parallel([progressAnimations, percentageAnim]).start(); } else { progressAnimations.start(); } }, [segmentTargets, animatedValue, indicator?.show, progressAnimatedValues]); const run = (0, _react.useCallback)(({ progress }) => { // Stop ongoing animations and clear all listeners animatedValue.stopAnimation(); animatedValue.removeAllListeners(); animatedValue.setValue(0); progressAnimatedValues.forEach(v => { v.stopAnimation(); v.removeAllListeners(); v.setValue(0); }); const targets = getProgressValues(progress); activeProgressRef.current = progress; // Set up indicator static properties once (not per-frame) if (indicator?.show && indicatorCircleRef.current && tSpanRef.current) { const calculatedProgress = `${Math.round(progress)}%`; // @ts-ignore – setNativeProps exists on native ref indicatorCircleRef.current.setNativeProps({ r: indicator.radius || 0, strokeWidth: indicator.strokeWidth || 0 }); // @ts-ignore – setNativeProps exists on native ref tSpanRef.current.setNativeProps({ children: calculatedProgress, font: INDICATOR_FONT }); } // Set up indicator position listener (trig-based, can't use interpolate) if (indicator?.show) { const meanGap = getMeanSegmentsGap(progress); animatedValue.addListener(v => { const val = Math.min(v.value, progress); const paintedLength = circleCircumference * val / 100; const adjustedLength = paintedLength - meanGap; if (adjustedLength <= 0) return; const { x: cx, y: cy } = (0, _helpers.getArcEndCoordinates)(radius, adjustedLength, halfCircle, halfCircle, rotation); if (indicatorCircleRef.current) { // @ts-ignore – setNativeProps exists on native ref indicatorCircleRef.current.setNativeProps({ cx, cy }); } if (tSpanRef.current) { // @ts-ignore – setNativeProps exists on native ref tSpanRef.current.setNativeProps({ dx: cx, dy: cy + 5 }); } }); } // Store pending config and trigger re-render with new targets. // Animations start in a useEffect AFTER React has re-rendered, // ensuring interpolations in progressTrack are set up correctly. pendingAnimationRef.current = { progress, targets }; setSegmentTargets(targets); }, [animatedValue, circleCircumference, getMeanSegmentsGap, getProgressValues, halfCircle, indicator?.show, indicator?.radius, indicator?.strokeWidth, progressAnimatedValues, radius, rotation]); // Memoize base track circles (static, never animate) const baseTrack = (0, _react.useMemo)(() => { const baseStrokeDashoffset = circleCircumference - circleCircumference / segments + segmentsGap; return new Array(segments).fill(null).map((_, key) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeSvg.Circle, { cx: halfCircle, cy: halfCircle, r: radius, stroke: baseColor, rotation: rotation + key * 360 / segments, origin: `${halfCircle}, ${halfCircle}`, strokeWidth: strokeWidth, strokeDasharray: circleCircumference, strokeDashoffset: baseStrokeDashoffset, strokeLinecap: "round" }, key)); }, [baseColor, circleCircumference, halfCircle, radius, rotation, segments, segmentsGap, strokeWidth]); // Memoize progress overlay circles with declarative interpolated strokeDashoffset const progressTrack = (0, _react.useMemo)(() => { return progressAnimatedValues.map((animVal, key) => { const target = segmentTargets[key] ?? 0; const strokeDashoffset = computeStrokeDashoffset(animVal, target, circleCircumference, segments, segmentsGap); return /*#__PURE__*/(0, _jsxRuntime.jsx)(ProgressCircle, { stroke: progressColor, cx: halfCircle, cy: halfCircle, r: radius, origin: `${halfCircle}, ${halfCircle}`, strokeWidth: strokeWidth, strokeDasharray: circleCircumference, strokeDashoffset: strokeDashoffset, rotation: rotation + key * 360 / segments, strokeLinecap: "round" }, key); }); }, [circleCircumference, halfCircle, progressAnimatedValues, progressColor, radius, rotation, segmentTargets, segments, segmentsGap, strokeWidth]); (0, _react.useImperativeHandle)(ref, () => ({ run }), [run]); return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeSvg.default, { viewBox: `0 0 ${halfCircle * 2} ${halfCircle * 2}`, width: '100%', fill: "none", height: radius * 2, children: [centerComponent && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.centerComponent, children: centerComponent }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeSvg.G, { children: [baseTrack, progressTrack, indicator?.show === true && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(IndicatorCircle, { stroke: progressColor, ref: indicatorCircleRef, fill: "white" }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeSvg.TSpan, { stroke: progressColor, fill: progressColor, ref: tSpanRef })] })] })] }); }; var _default = exports.default = /*#__PURE__*/(0, _react.memo)(/*#__PURE__*/(0, _react.forwardRef)(RNSegmentedProgressBar)); const styles = _reactNative.StyleSheet.create({ centerComponent: { height: '100%', justifyContent: 'center', alignItems: 'center' } }); //# sourceMappingURL=index.js.map