diff --git a/CHANGELOG.md b/CHANGELOG.md index 616b8c8..a441893 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,11 @@ +### Build: 🏠 `4.4.0` - react-native-circular-progress-indicator + +--- +- feat: add imperative methods to play, pause, and reAnimate ### Build: 🏠 `4.3.0` - react-native-circular-progress-indicator --- - feat: change stroke color based on animation value -- ### Build: 🏠 `4.2.1` - react-native-circular-progress-indicator --- diff --git a/README.md b/README.md index 5ca041c..1b6cb63 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ This project is inspired from this [Youtube tutorial](https://www.youtube.com/wa ![](examples/demo8.gif) ![](examples/demo9.gif) ![](examples/demo11.gif) +![](examples/demo12.gif) ## Prerequisites @@ -326,8 +327,35 @@ import CircularProgress from 'react-native-circular-progress-indicator'; ]} /> ``` - ![](examples/demo11.gif) +#### Play, Pause, and ReAnimate + +```jsx +import CircularProgress, { ProgressRef } from 'react-native-circular-progress-indicator'; + +const progressRef = useRef(null); + +// to pause animation +progressRef.current.pause(); + +// to play animation +progressRef.current.play(); + +// to re-play animation +progressRef.current.reAnimate(); + +.... + + +``` + +![](examples/demo12.gif) + ## Props ## CircularProgressBase Props @@ -381,5 +409,28 @@ CircularProgress component accepts all CircularProgressBase props except the chi | valuePrefixStyle | custom styling to value prefix. Use this to customize the styling of the value prefix. If not provided, the progress value style/colors will be used. | TextStyle | {} | false | | valueSuffixStyle | custom styling to value suffix. Use this to customize the styling of the value suffix. If not provided, the progress value style/colors will be used. | TextStyle | {} | false | +## Methods + +`pause` +Imperative method to pause the animation. + +```javascript +progressRef.current.pause(); +``` + +`play` +Imperative method to play the animation once paused. + +```javascript +progressRef.current.play(); +``` + +`reAnimate` +Imperative method to restart the animation. + +```javascript +progressRef.current.reAnimate(); +``` + ## License -This project is licenced under the MIT License. +This project is licensed under the MIT License. diff --git a/examples/demo12.gif b/examples/demo12.gif new file mode 100644 index 0000000..381d7a6 Binary files /dev/null and b/examples/demo12.gif differ diff --git a/package.json b/package.json index da760b9..c48a698 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-circular-progress-indicator", - "version": "4.3.0", + "version": "4.4.0", "description": "React Native customizable circular progress indicator", "main": "lib/commonjs/index", "module": "lib/module/index", @@ -125,5 +125,8 @@ "/lib/" ], "transformIgnorePatterns": [] + }, + "dependencies": { + "react-native-redash": "*" } } diff --git a/src/circularProgress/index.tsx b/src/circularProgress/index.tsx index 4ed2fbf..318dbf2 100644 --- a/src/circularProgress/index.tsx +++ b/src/circularProgress/index.tsx @@ -1,59 +1,68 @@ -import React, {useMemo} from 'react'; -import {Text, StyleSheet, View} from 'react-native'; +import React, { forwardRef, useImperativeHandle, useMemo } from 'react'; +import { Text, StyleSheet, View } from 'react-native'; import ProgressCircle from '../components/progressCircle'; import useAnimatedValue from '../hooks/useAnimatedValue'; import COLORS from '../utils/colors'; -import type {CircularProgressProps} from '../types'; +import type { CircularProgressProps, ProgressRef } from '../types'; import ProgressValue from '../components/progressValue'; import styles from './styles'; -const CircularProgress: React.FC = ({ - value, - initialValue = 0, - circleBackgroundColor = COLORS.TRANSPARENT, - radius = 60, - duration = 500, - delay = 0, - maxValue = 100, - strokeLinecap = 'round', - onAnimationComplete = () => null, - activeStrokeColor = COLORS.GREEN, - activeStrokeSecondaryColor = null, - activeStrokeWidth = 10, - inActiveStrokeColor = COLORS.BLACK_30, - inActiveStrokeWidth = 10, - inActiveStrokeOpacity = 1, - clockwise = true, - rotation = 0, - title = '', - titleStyle = {}, - titleColor, - titleFontSize, - progressValueColor, - progressValueStyle = {}, - progressValueFontSize, - valuePrefix = '', - valueSuffix = '', - showProgressValue = true, - subtitle = '', - subtitleStyle = {}, - subtitleColor, - subtitleFontSize, - progressFormatter = (v: number) => { - 'worklet'; +// eslint-disable-next-line max-len, prettier/prettier +const CircularProgress = forwardRef((props, ref) => { + const { + value, + initialValue = 0, + circleBackgroundColor = COLORS.TRANSPARENT, + radius = 60, + duration = 500, + delay = 0, + maxValue = 100, + strokeLinecap = 'round', + onAnimationComplete = () => null, + activeStrokeColor = COLORS.GREEN, + activeStrokeSecondaryColor = null, + activeStrokeWidth = 10, + inActiveStrokeColor = COLORS.BLACK_30, + inActiveStrokeWidth = 10, + inActiveStrokeOpacity = 1, + clockwise = true, + rotation = 0, + title = '', + titleStyle = {}, + titleColor, + titleFontSize, + progressValueColor, + progressValueStyle = {}, + progressValueFontSize, + valuePrefix = '', + valueSuffix = '', + showProgressValue = true, + subtitle = '', + subtitleStyle = {}, + subtitleColor, + subtitleFontSize, + progressFormatter = (v: number) => { + 'worklet'; - return Math.round(v); - }, - allowFontScaling = true, - dashedStrokeConfig = {count: 0, width: 0}, - valuePrefixStyle = {}, - valueSuffixStyle = {}, - strokeColorConfig = undefined, -}: CircularProgressProps) => { - const {animatedCircleProps, animatedTextProps, progressValue} = - useAnimatedValue({ + return Math.round(v); + }, + allowFontScaling = true, + dashedStrokeConfig = { count: 0, width: 0 }, + valuePrefixStyle = {}, + valueSuffixStyle = {}, + strokeColorConfig = undefined, + } = props; + + const { + animatedCircleProps, + animatedTextProps, + progressValue, + play, + pause, + reAnimate, + } = useAnimatedValue({ initialValue, radius, maxValue, @@ -68,6 +77,12 @@ const CircularProgress: React.FC = ({ strokeColorConfig, }); + useImperativeHandle(ref, () => ({ + play, + pause, + reAnimate, + })); + const styleProps = useMemo( () => ({ radius, @@ -189,6 +204,6 @@ const CircularProgress: React.FC = ({ ); -}; +}); export default CircularProgress; diff --git a/src/circularProgressBase/index.tsx b/src/circularProgressBase/index.tsx index 4d83a62..3c6d732 100644 --- a/src/circularProgressBase/index.tsx +++ b/src/circularProgressBase/index.tsx @@ -1,36 +1,39 @@ -import React, {useMemo} from 'react'; +import React, {forwardRef, useImperativeHandle, useMemo} from 'react'; import {StyleSheet, View} from 'react-native'; import ProgressCircle from '../components/progressCircle'; import useAnimatedValue from '../hooks/useAnimatedValue'; import COLORS from '../utils/colors'; -import type {CircularProgressBaseProps} from '../types'; +import type {CircularProgressBaseProps, ProgressRef} from '../types'; import styles from './styles'; -const CircularProgressBase: React.FC = ({ - value, - initialValue = 0, - circleBackgroundColor = COLORS.TRANSPARENT, - radius = 60, - duration = 500, - delay = 0, - maxValue = 100, - strokeLinecap = 'round', - onAnimationComplete = () => null, - activeStrokeColor = COLORS.GREEN, - activeStrokeSecondaryColor = null, - activeStrokeWidth = 10, - inActiveStrokeColor = COLORS.BLACK_30, - inActiveStrokeWidth = 10, - inActiveStrokeOpacity = 1, - clockwise = true, - rotation = 0, - dashedStrokeConfig = {count: 0, width: 0}, - strokeColorConfig = undefined, - children, -}: CircularProgressBaseProps) => { - const {animatedCircleProps} = useAnimatedValue({ +// eslint-disable-next-line max-len, prettier/prettier +const CircularProgressBase = forwardRef((props, ref) => { + const { + value, + initialValue = 0, + circleBackgroundColor = COLORS.TRANSPARENT, + radius = 60, + duration = 500, + delay = 0, + maxValue = 100, + strokeLinecap = 'round', + onAnimationComplete = () => null, + activeStrokeColor = COLORS.GREEN, + activeStrokeSecondaryColor = null, + activeStrokeWidth = 10, + inActiveStrokeColor = COLORS.BLACK_30, + inActiveStrokeWidth = 10, + inActiveStrokeOpacity = 1, + clockwise = true, + rotation = 0, + dashedStrokeConfig = {count: 0, width: 0}, + strokeColorConfig = undefined, + children, + } = props; + + const {animatedCircleProps, play, pause, reAnimate} = useAnimatedValue({ initialValue, radius, maxValue, @@ -44,6 +47,12 @@ const CircularProgressBase: React.FC = ({ strokeColorConfig, }); + useImperativeHandle(ref, () => ({ + play, + pause, + reAnimate, + })); + const styleProps = useMemo( () => ({ radius, @@ -78,6 +87,6 @@ const CircularProgressBase: React.FC = ({ ); -}; +}); export default CircularProgressBase; diff --git a/src/hooks/useAnimatedValue.ts b/src/hooks/useAnimatedValue.ts index dbf1354..1ec67cb 100644 --- a/src/hooks/useAnimatedValue.ts +++ b/src/hooks/useAnimatedValue.ts @@ -1,4 +1,4 @@ -import {useEffect, useMemo} from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import { Easing, interpolateColor, @@ -9,6 +9,7 @@ import { withDelay, withTiming, } from 'react-native-reanimated'; +import { withPause } from 'react-native-redash'; import type { StrokeColorConfigType } from '../types'; @@ -55,25 +56,66 @@ export default function useAnimatedValue({ }, strokeColorConfig = undefined, }: UseAnimatedValueProps) { + const paused = useSharedValue(false); const animatedValue = useSharedValue(initialValue); const { circleCircumference } = useCircleValues({ radius, activeStrokeWidth, inActiveStrokeWidth, + // eslint-disable-next-line prettier/prettier }); + const pause = useCallback(() => { + paused.value = true; + }, [paused]); + + const play = useCallback(() => { + paused.value = false; + }, [paused]); + + const resetAnimatedValue = useCallback(() => { + paused.value = false; + animatedValue.value = initialValue; + }, [animatedValue, initialValue, paused]); + + const animateValue = useCallback(() => { + animatedValue.value = withPause( + withDelay( + delay, + withTiming(value, { duration, easing: Easing.linear }, (isFinished) => { + if (isFinished) { + runOnJS(onAnimationComplete)?.(); + } + }) + ), + paused + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + },[animatedValue, delay, duration, paused, value]); + + const reAnimate = () => { + resetAnimatedValue(); + animateValue(); + }; + const sortedStrokeColors = useMemo(() => { - if (!strokeColorConfig) {return null;} + if (!strokeColorConfig) { + return null; + } return strokeColorConfig.sort((a, b) => a.value - b.value); }, [strokeColorConfig]); const colors = useMemo(() => { - if (!sortedStrokeColors) {return null;} + if (!sortedStrokeColors) { + return null; + } return sortedStrokeColors.map((item) => item.color); }, [sortedStrokeColors]); const values = useMemo(() => { - if (!sortedStrokeColors) {return null;} + if (!sortedStrokeColors) { + return null; + } return sortedStrokeColors.map((item) => item.value); }, [sortedStrokeColors]); @@ -83,31 +125,23 @@ export default function useAnimatedValue({ const maxPercentage: number = clockwise ? (100 * animatedValue.value) / biggestValue : (100 * -animatedValue.value) / biggestValue; - const config: Config = { - strokeDashoffset: - circleCircumference - (circleCircumference * maxPercentage) / 100, - }; - const strokeColor = - colors && values - ? interpolateColor(animatedValue.value, values, colors) - : undefined; - if (strokeColor) { - config.stroke = strokeColor; - } - return config; + const config: Config = { + strokeDashoffset: + circleCircumference - (circleCircumference * maxPercentage) / 100, + }; + const strokeColor = + colors && values + ? interpolateColor(animatedValue.value, values, colors) + : undefined; + if (strokeColor) { + config.stroke = strokeColor; + } + return config; }); useEffect(() => { - animatedValue.value = withDelay( - delay, - withTiming(value, { duration, easing: Easing.linear }, (isFinished) => { - if (isFinished) { - runOnJS(onAnimationComplete)?.(); - } - }) - ); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [value]); + animateValue(); + }, [animateValue]); const progressValue = useDerivedValue(() => { return `${progressFormatter(animatedValue.value)}`; @@ -116,13 +150,16 @@ export default function useAnimatedValue({ const animatedTextProps = useAnimatedProps(() => { return { text: progressValue.value, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any; // eslint-disable-line prettier/prettier + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any; }); return { animatedCircleProps, animatedTextProps, progressValue, + pause, + play, + reAnimate, }; } diff --git a/src/index.ts b/src/index.ts index 14ccc7e..499b0a6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,12 @@ import CircularProgress from './circularProgress'; import CircularProgressBase from './circularProgressBase'; -import type {DashedStrokeConfigType, StrokeColorConfigType} from './types'; +import type { + DashedStrokeConfigType, + StrokeColorConfigType, + ProgressRef, +} from './types'; export default CircularProgress; export {CircularProgressBase}; export {CircularProgressBase as CircularProgressWithChild}; -export type {DashedStrokeConfigType, StrokeColorConfigType}; +export type {DashedStrokeConfigType, StrokeColorConfigType, ProgressRef}; diff --git a/src/types/index.ts b/src/types/index.ts index cdf4292..0dfadd4 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -330,6 +330,21 @@ type ProgressValueProps = { allowFontScaling?: boolean, }; +type ProgressRef = { + /** + * Use this to play the animation once the animation is paused. + */ + play: () => void, + /** + * Use this to pause the animation. + */ + pause: () => void, + /** + * Use this to replay the animation. + */ + reAnimate: () => void, +}; + export type { CircleGradientProps, ProgressCircleProps, @@ -339,4 +354,5 @@ export type { DashedStrokeConfigType, ProgressValueProps, StrokeColorConfigType, + ProgressRef, }; diff --git a/yarn.lock b/yarn.lock index 59878b6..e76e482 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2760,6 +2760,11 @@ abort-controller@^3.0.0: dependencies: event-target-shim "^5.0.0" +abs-svg-path@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/abs-svg-path/-/abs-svg-path-0.1.1.tgz#df601c8e8d2ba10d4a76d625e236a9a39c2723bf" + integrity sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA== + absolute-path@^0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/absolute-path/-/absolute-path-0.0.0.tgz#a78762fbdadfb5297be99b15d35a785b2f095bf7" @@ -6611,6 +6616,13 @@ normalize-path@^3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== +normalize-svg-path@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/normalize-svg-path/-/normalize-svg-path-1.1.0.tgz#0e614eca23c39f0cffe821d6be6cd17e569a766c" + integrity sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg== + dependencies: + svg-arc-to-cubic-bezier "^3.0.0" + npm-run-path@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" @@ -6898,6 +6910,11 @@ parse-json@^5.0.0, parse-json@^5.2.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" +parse-svg-path@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/parse-svg-path/-/parse-svg-path-0.1.2.tgz#7a7ec0d1eb06fa5325c7d3e009b859a09b5d49eb" + integrity sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ== + parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" @@ -7200,6 +7217,15 @@ react-native-reanimated@^2.2.3: setimmediate "^1.0.5" string-hash-64 "^1.0.3" +react-native-redash@*: + version "18.0.0" + resolved "https://registry.yarnpkg.com/react-native-redash/-/react-native-redash-18.0.0.tgz#c075e8bff34d9aad2ce82ffdcb228e90e3a9cc4a" + integrity sha512-yyjbNxGPsKh10r4HvudNLJoX9fUsvGbcbmWlUE9pAWg3BCtFsImSEANFFNOmYbPk1JqnVqrWMGCmVvd4zfwn7Q== + dependencies: + abs-svg-path "^0.1.1" + normalize-svg-path "^1.0.1" + parse-svg-path "^0.1.2" + react-native-svg@^12.1.1: version "12.3.0" resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-12.3.0.tgz#40f657c5d1ee366df23f3ec8dae76fd276b86248" @@ -8048,6 +8074,11 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +svg-arc-to-cubic-bezier@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz#390c450035ae1c4a0104d90650304c3bc814abe6" + integrity sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g== + temp@0.8.3: version "0.8.3" resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.3.tgz#e0c6bc4d26b903124410e4fed81103014dfc1f59"