├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── example-app ├── .gitignore ├── .watchmanconfig ├── App.tsx ├── app.json ├── assets │ ├── icon.png │ └── splash.png ├── babel.config.js ├── package.json ├── tsconfig.json └── yarn.lock ├── index.d.ts ├── index.js ├── package-lock.json ├── package.json ├── screenshot.gif └── src ├── AnimatedCircularProgress.js └── CircularProgress.js /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | !**/*.xcodeproj 7 | !**/*.pbxproj 8 | !**/*.xcworkspacedata 9 | !**/*.xcsettings 10 | !**/*.xcscheme 11 | build/ 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata 21 | *.xccheckout 22 | *.moved-aside 23 | DerivedData 24 | *.hmap 25 | *.ipa 26 | *.xcuserstate 27 | project.xcworkspace 28 | 29 | # node.js 30 | # 31 | node_modules/ 32 | npm-debug.log 33 | 34 | #IDEA 35 | .idea -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | 25 | # node.js 26 | # 27 | node_modules/ 28 | npm-debug.log 29 | 30 | example-app/ 31 | 32 | screenshot.gif 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Bart Gryszko 4 | 5 | 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | THE SOFTWARE. 28 | 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-circular-progress 2 | 3 | [![license](https://img.shields.io/github/license/mashape/apistatus.svg)]() 4 | [![Version](https://img.shields.io/npm/v/react-native-circular-progress.svg)](https://www.npmjs.com/package/react-native-circular-progress) 5 | [![npm](https://img.shields.io/npm/dt/react-native-circular-progress.svg)](https://www.npmjs.com/package/react-native-circular-progress) 6 | 7 | 8 | React Native component for creating animated, circular progress. Useful for displaying users points for example. 9 | 10 | ## Example app 11 | 12 | ![image](screenshot.gif) 13 | 14 | ## Installation 15 | 16 | 1. Install this component and `react-native-svg`: 17 | 18 | `npm i --save react-native-circular-progress react-native-svg` 19 | 20 | 2. Link native code for SVG: 21 | 22 | `react-native link react-native-svg` 23 | 24 | ## Usage 25 | ```js 26 | import { AnimatedCircularProgress } from 'react-native-circular-progress'; 27 | 28 | console.log('onAnimationComplete')} 34 | backgroundColor="#3d5875" /> 35 | ``` 36 | 37 | You can also define a function that'll receive current progress and for example display it inside the circle: 38 | 39 | ```jsx 40 | 46 | { 47 | (fill) => ( 48 | 49 | { this.state.fill } 50 | 51 | ) 52 | } 53 | 54 | ``` 55 | 56 | You can also define a function that'll receive the location at the top of the progress circle and render a custom SVG element: 57 | 58 | ```jsx 59 | } 67 | /> 68 | ``` 69 | 70 | Finally, you can manually trigger a duration-based timing animation by putting a ref on the component and calling the `animate(toValue, duration, easing)` function like so: 71 | 72 | ```jsx 73 | this.circularProgress = ref} 75 | ... 76 | /> 77 | ``` 78 | 79 | ```js 80 | this.circularProgress.animate(100, 8000, Easing.quad); // Will fill the progress bar linearly in 8 seconds 81 | ``` 82 | 83 | The `animate`-function returns the timing animation so you can chain, run in parallel etc. 84 | 85 | ## Configuration 86 | 87 | You can configure the CircularProgress-component by passing the following props: 88 | 89 | Name | Type | Default value | Description 90 | ----------------------|------------------------|-------------------------|-------------- 91 | size | number\|Animated.Value | **required** | Width and height of circle 92 | width | number | **required** | Thickness of the progress line 93 | backgroundWidth | number | width | Thickness of background circle 94 | fill | number (0-100) | 0 | Current progress / fill 95 | tintColor | string | black | Color of the progress line 96 | tintTransparency | boolean | true | Transparency of the progress line 97 | backgroundColor | string | | If unspecified, no background line will be rendered 98 | rotation | number (-360 - 360) | 90 | Angle from which the progress starts from 99 | lineCap | string | butt | Shape used at ends of progress line. Possible values: butt, round, square 100 | arcSweepAngle | number (0-360) | 360 | If you don't want a full circle, specify the arc angle 101 | style | ViewPropTypes.style | | Extra styling for the main container 102 | children | function | | Pass a function as a child. It received the current fill-value as an argument 103 | childrenContainerStyle| ViewPropTypes.style | | Extra styling for the children container 104 | padding | number | 0 | Padding applied around the circle to allow for a cap that bleeds outside its boundary 105 | dashedBackground | object | { width: 0, gap: 0 } | Bar background as dashed type 106 | dashedTint | object | { width: 0, gap: 0 } | Bar tint as dashed type 107 | renderCap | function | undefined | Function that's invoked during rendering to draw at the tip of the progress circle 108 | 109 | The following props can further be used on `AnimatedCircularProgress`: 110 | 111 | Name | Type | Default value | Description 112 | --------------------|------------------------|-------------------------|-------------- 113 | prefill | number (0-100) | 0 | Initial fill-value before animation starts 114 | duration | number | 500 | Duration of animation in ms 115 | delay | number | 0 | Delay of animation in ms 116 | easing | function | Easing.out(Easing.ease) | Animation easing function 117 | onAnimationComplete | function | | Function that's invoked when the animation completes (both on mount and if called with `.animate()`) 118 | onFillChange | function | | Function that returns current progress on every change 119 | tintColorSecondary | string | the same as tintColor | To change fill color from tintColor to tintColorSecondary as animation progresses 120 | 121 | `AnimatedCircularProgress` also exposes the following functions: 122 | 123 | Name | Arguments | Description 124 | ------------|----------- |---------------- 125 | animate | (toVal: number, duration: number, ease: function) | Animate the progress bar to a specific value 126 | reAnimate | (prefill: number, toVal: number, duration: number, ease: function) | Re-run animation with a specified prefill-value 127 | 128 | ## Running example app (Expo) 129 | 130 | ```sh 131 | git clone https://github.com/bgryszko/react-native-circular-progress.git 132 | cd react-native-circular-progress/example-app 133 | yarn 134 | yarn start 135 | ``` 136 | 137 | ## Authors 138 | 139 | * Bartosz Gryszko (b@gryszko.com) 140 | * Markus Lindqvist 141 | * Jacob Lauritzen 142 | * Special thanks to all contributors! 143 | 144 | ## License 145 | 146 | MIT 147 | 148 | ## Special thanks 149 | 150 | Special thanks to [Chalk+Chisel](http://chalkchisel.com) for creating working environment where people grow. This component was created for one of the projects we're working on. 151 | -------------------------------------------------------------------------------- /example-app/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | *.jks 5 | *.p12 6 | *.key 7 | *.mobileprovision 8 | *.orig.* 9 | web-build/ 10 | web-report/ 11 | -------------------------------------------------------------------------------- /example-app/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /example-app/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, Text, PanResponder, View, PanResponderInstance } from 'react-native'; 3 | import { AnimatedCircularProgress } from 'react-native-circular-progress'; 4 | 5 | const MAX_POINTS = 500; 6 | export default class App extends React.Component { 7 | state = { 8 | isMoving: false, 9 | pointsDelta: 0, 10 | points: 325, 11 | }; 12 | 13 | _panResponder : PanResponderInstance; 14 | _circularProgressRef: React.RefObject; 15 | 16 | constructor(props: Readonly<{}>) { 17 | super(props); 18 | this._circularProgressRef = React.createRef(); 19 | this._panResponder = PanResponder.create({ 20 | onStartShouldSetPanResponder: (evt, gestureState) => true, 21 | onStartShouldSetPanResponderCapture: (evt, gestureState) => true, 22 | onMoveShouldSetPanResponder: (evt, gestureState) => true, 23 | onMoveShouldSetPanResponderCapture: (evt, gestureState) => true, 24 | 25 | onPanResponderGrant: (evt, gestureState) => { 26 | this.setState({ isMoving: true, pointsDelta: 0 }); 27 | }, 28 | 29 | onPanResponderMove: (evt, gestureState) => { 30 | if (this._circularProgressRef.current) { 31 | this._circularProgressRef.current.animate(0, 0); 32 | } 33 | // For each 2 pixels add or subtract 1 point 34 | this.setState({ pointsDelta: Math.round(-gestureState.dy / 2) }); 35 | }, 36 | onPanResponderTerminationRequest: (evt, gestureState) => true, 37 | onPanResponderRelease: (evt, gestureState) => { 38 | if (this._circularProgressRef.current) { 39 | this._circularProgressRef.current.animate(100, 3000); 40 | } 41 | let points = this.state.points + this.state.pointsDelta; 42 | console.log(Math.min(points, MAX_POINTS)); 43 | this.setState({ 44 | isMoving: false, 45 | points: points > 0 ? Math.min(points, MAX_POINTS) : 0, 46 | pointsDelta: 0, 47 | }); 48 | }, 49 | }); 50 | } 51 | 52 | render() { 53 | const fill = (this.state.points / MAX_POINTS) * 100; 54 | return ( 55 | 56 | 64 | {fill => {Math.round((MAX_POINTS * fill) / 100)}} 65 | 66 | 67 | 79 | 80 | console.log('onAnimationComplete')} 86 | ref={this._circularProgressRef} 87 | backgroundColor="#3d5875" 88 | arcSweepAngle={180} 89 | /> 90 | 91 | 92 | {this.state.pointsDelta >= 0 && '+'} 93 | {this.state.pointsDelta} 94 | 95 | 96 | ); 97 | } 98 | } 99 | 100 | const styles = StyleSheet.create({ 101 | points: { 102 | textAlign: 'center', 103 | color: '#7591af', 104 | fontSize: 50, 105 | fontWeight: '100', 106 | }, 107 | container: { 108 | flex: 1, 109 | justifyContent: 'space-between', 110 | alignItems: 'center', 111 | backgroundColor: '#152d44', 112 | padding: 50, 113 | }, 114 | pointsDelta: { 115 | color: '#4c6479', 116 | fontSize: 50, 117 | fontWeight: '100', 118 | }, 119 | pointsDeltaActive: { 120 | color: '#fff', 121 | }, 122 | }); 123 | -------------------------------------------------------------------------------- /example-app/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "circular-progress-example", 4 | "slug": "example-app", 5 | "privacy": "public", 6 | "sdkVersion": "33.0.0", 7 | "platforms": [ 8 | "ios", 9 | "android", 10 | "web" 11 | ], 12 | "version": "1.0.0", 13 | "orientation": "portrait", 14 | "icon": "./assets/icon.png", 15 | "splash": { 16 | "image": "./assets/splash.png", 17 | "resizeMode": "contain", 18 | "backgroundColor": "#ffffff" 19 | }, 20 | "updates": { 21 | "fallbackToCacheTimeout": 0 22 | }, 23 | "assetBundlePatterns": [ 24 | "**/*" 25 | ], 26 | "ios": { 27 | "supportsTablet": true 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /example-app/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartgryszko/react-native-circular-progress/ad55db33198190c8fd89a814b604f98d505784af/example-app/assets/icon.png -------------------------------------------------------------------------------- /example-app/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartgryszko/react-native-circular-progress/ad55db33198190c8fd89a814b604f98d505784af/example-app/assets/splash.png -------------------------------------------------------------------------------- /example-app/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /example-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "node_modules/expo/AppEntry.js", 3 | "scripts": { 4 | "start": "expo start", 5 | "android": "expo start --android", 6 | "ios": "expo start --ios", 7 | "web": "expo start --web", 8 | "eject": "expo eject" 9 | }, 10 | "dependencies": { 11 | "expo": "^33.0.0", 12 | "react": "16.8.3", 13 | "react-dom": "^16.8.6", 14 | "react-native": "https://github.com/expo/react-native/archive/sdk-33.0.0.tar.gz", 15 | "react-native-circular-progress": "git+https://github.com/bgryszko/react-native-circular-progress.git", 16 | "react-native-web": "^0.11.4" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^16.8.19", 20 | "@types/react-native": "^0.57.60", 21 | "babel-preset-expo": "^5.1.1", 22 | "typescript": "^3.4.5" 23 | }, 24 | "private": true 25 | } 26 | -------------------------------------------------------------------------------- /example-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "noEmit": true, 5 | "lib": ["dom", "esnext"], 6 | "jsx": "react-native", 7 | "moduleResolution": "node", 8 | "allowSyntheticDefaultImports": true, 9 | "skipLibCheck": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-native-circular-progress' { 2 | import * as React from 'react'; 3 | import { 4 | Animated, 5 | EasingFunction, 6 | StyleProp, 7 | ViewStyle 8 | } from 'react-native'; 9 | 10 | export interface AnimatedCircularProgressProps extends CircularProgressProps { 11 | 12 | /** 13 | * Angle from which the progress starts from 14 | * 15 | * @type {number} 16 | * @default 90 17 | */ 18 | rotation?: number; 19 | 20 | /** 21 | * Initial fill-value before animation starts 22 | * 23 | * @type {number} 24 | * @default 0 25 | */ 26 | prefill?: number; 27 | 28 | /** 29 | * Duration of animation in ms 30 | * 31 | * @type {number} 32 | * @default 500 33 | */ 34 | duration?: number; 35 | 36 | /** 37 | * Delay of animation in ms 38 | * 39 | * @type {number} 40 | * @default 0 41 | */ 42 | delay?: number; 43 | 44 | /** 45 | * 46 | * @type {Function} 47 | * @default Easing.out(Easing.ease) 48 | */ 49 | easing?: EasingFunction; 50 | 51 | /** 52 | * Function that's invoked when the animation completes (both on mount and if called with .animate()) 53 | * 54 | */ 55 | onAnimationComplete?: (event: { finished: boolean }) => void; 56 | } 57 | 58 | export class AnimatedCircularProgress extends React.Component< 59 | AnimatedCircularProgressProps 60 | > { 61 | /** 62 | * Animate the progress bar to a specific value 63 | * 64 | * @param {number} toVal 65 | * @param {number} duration 66 | * @param {Function} ease 67 | */ 68 | animate: (toVal: number, duration: number, ease?: EasingFunction) => Animated.CompositeAnimation; 69 | 70 | /** 71 | * Re-run animation with a specified prefill-value 72 | * 73 | * @param {number} prefill 74 | * @param {number} toVal 75 | * @param {number} duration 76 | * @param {Function} ease 77 | */ 78 | reAnimate: ( 79 | prefill: number, 80 | toVal: number, 81 | duration: number, 82 | ease?: EasingFunction 83 | ) => void; 84 | } 85 | 86 | export interface CircularProgressProps { 87 | /** 88 | * Style of the entire progress container 89 | * 90 | * @type {StyleProp} 91 | */ 92 | style?: StyleProp; 93 | /** 94 | * Width and height of circle 95 | * 96 | * @type {number | Animated.Value} 97 | */ 98 | size: number | Animated.Value; 99 | 100 | /** 101 | * Thickness of the progress line 102 | * 103 | * @type {number} 104 | */ 105 | width: number; 106 | 107 | /** 108 | * Current progress / fill 109 | * 110 | * @type {number} 111 | */ 112 | fill: number; 113 | 114 | /** 115 | * Thickness of background circle 116 | * 117 | * @type {number} 118 | * @default width 119 | */ 120 | backgroundWidth?: number; 121 | 122 | /** 123 | * Color of the progress line 124 | * 125 | * @type {string} 126 | * @default 'black' 127 | */ 128 | tintColor?: string; 129 | 130 | 131 | /** 132 | * Current progress / tint transparency 133 | * 134 | * @type {boolean} 135 | * @default true 136 | */ 137 | tintTransparency?: boolean; 138 | 139 | /** 140 | * If unspecified, no background line will be rendered 141 | * 142 | * @type {string} 143 | */ 144 | backgroundColor?: string; 145 | 146 | /** 147 | * Angle from which the progress starts from 148 | * 149 | * @type {number} 150 | * @default 90 151 | */ 152 | rotation?: number; 153 | 154 | /** 155 | * Shape used at ends of progress line. 156 | * 157 | * @type {('butt' | 'round' | 'square')} 158 | * @default 'butt' 159 | */ 160 | lineCap?: 'butt' | 'round' | 'square'; 161 | 162 | /** 163 | * Shape used at ends of progress line. 164 | * 165 | * @type {('butt' | 'round' | 'square')} 166 | * @default lineCap - which is 'butt' 167 | */ 168 | fillLineCap?: 'butt' | 'round' | 'square'; 169 | 170 | /** 171 | * If you don't want a full circle, specify the arc angle 172 | * 173 | * @type {number} 174 | * @default 360 175 | */ 176 | arcSweepAngle?: number; 177 | 178 | /** 179 | * Pass a function as a child. It receiveds the current fill-value as an argument 180 | * 181 | * @type {Function} 182 | * @param {number} fill current fill-value 183 | * @return {JSX.Element} the element inside the circle 184 | */ 185 | children?: ((fill: number) => JSX.Element) | React.ReactChild; 186 | 187 | /** 188 | * Style of the children container 189 | * 190 | * @type {StyleProp} 191 | */ 192 | childrenContainerStyle?: StyleProp; 193 | 194 | /** 195 | * Padding applied around the circle to allow for a cap that bleeds outside its boundary 196 | * 197 | * @type {number} 198 | * @default 0 199 | */ 200 | padding?: number; 201 | 202 | /** 203 | * Function that's invoked during rendering to draw at the tip of the progress circle 204 | * 205 | */ 206 | renderCap?: (payload: { 207 | center: { x: number; y: number }; 208 | }) => React.ReactNode; 209 | 210 | /** 211 | * Use dashed type for tint/progress line 212 | * 213 | * @type { width: number; gap: number } 214 | * @default '{ width: 0, gap: 0 }' 215 | */ 216 | dashedTint?: { width: number; gap: number }; 217 | 218 | /** 219 | * Use dashed type for background 220 | * 221 | * @type { width: number; gap: number } 222 | * @default '{ width: 0, gap: 0 }' 223 | */ 224 | dashedBackground?: { width: number; gap: number }; 225 | } 226 | 227 | export class CircularProgress extends React.Component { 228 | 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import CircularProgress from './src/CircularProgress'; 2 | import AnimatedCircularProgress from './src/AnimatedCircularProgress'; 3 | 4 | export { CircularProgress, AnimatedCircularProgress } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-circular-progress", 3 | "version": "1.4.1", 4 | "description": "React Native component for creating animated, circular progress with react-native-svg", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+ssh://git@github.com/bgryszko/react-native-circular-progress.git" 9 | }, 10 | "keywords": [ 11 | "react", 12 | "react-native", 13 | "react-component", 14 | "progress", 15 | "chart", 16 | "react-svg" 17 | ], 18 | "author": { 19 | "name": "Bart Gryszko", 20 | "email": "b@gryszko.com" 21 | }, 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/bgryszko/react-native-circular-progress/issues" 25 | }, 26 | "homepage": "https://github.com/bgryszko/react-native-circular-progress", 27 | "dependencies": { 28 | "prop-types": "^15.8.1" 29 | }, 30 | "peerDependencies": { 31 | "react": ">=16.0.0", 32 | "react-native": ">=0.50.0", 33 | "react-native-svg": ">=7.0.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartgryszko/react-native-circular-progress/ad55db33198190c8fd89a814b604f98d505784af/screenshot.gif -------------------------------------------------------------------------------- /src/AnimatedCircularProgress.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Animated, Easing } from "react-native"; 4 | import CircularProgress from "./CircularProgress"; 5 | const AnimatedProgress = Animated.createAnimatedComponent(CircularProgress); 6 | 7 | export default class AnimatedCircularProgress extends React.PureComponent { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | fillAnimation: new Animated.Value(props.prefill), 12 | }; 13 | if (props.onFillChange) { 14 | this.state.fillAnimation.addListener(({ value }) => 15 | props.onFillChange(value) 16 | ); 17 | } 18 | } 19 | 20 | componentDidMount() { 21 | this.animate(); 22 | } 23 | 24 | componentDidUpdate(prevProps) { 25 | if (prevProps.fill !== this.props.fill) { 26 | this.animate(); 27 | } 28 | } 29 | 30 | reAnimate(prefill, toVal, dur, ease) { 31 | this.setState( 32 | { 33 | fillAnimation: new Animated.Value(prefill), 34 | }, 35 | () => this.animate(toVal, dur, ease) 36 | ); 37 | } 38 | 39 | animate(toVal, dur, ease) { 40 | const toValue = toVal >= 0 ? toVal : this.props.fill; 41 | const duration = dur || this.props.duration; 42 | const easing = ease || this.props.easing; 43 | const useNativeDriver = this.props.useNativeDriver; 44 | const delay = this.props.delay; 45 | 46 | const anim = Animated.timing(this.state.fillAnimation, { 47 | useNativeDriver, 48 | toValue, 49 | easing, 50 | duration, 51 | delay, 52 | }); 53 | anim.start(this.props.onAnimationComplete); 54 | 55 | return anim; 56 | } 57 | 58 | animateColor() { 59 | if (!this.props.tintColorSecondary) { 60 | return this.props.tintColor; 61 | } 62 | 63 | const tintAnimation = this.state.fillAnimation.interpolate({ 64 | inputRange: [0, 100], 65 | outputRange: [this.props.tintColor, this.props.tintColorSecondary], 66 | }); 67 | 68 | return tintAnimation; 69 | } 70 | 71 | render() { 72 | const { fill, prefill, ...other } = this.props; 73 | 74 | return ( 75 | 80 | ); 81 | } 82 | } 83 | 84 | AnimatedCircularProgress.propTypes = { 85 | ...CircularProgress.propTypes, 86 | prefill: PropTypes.number, 87 | tintColorSecondary: PropTypes.string, 88 | duration: PropTypes.number, 89 | easing: PropTypes.func, 90 | onAnimationComplete: PropTypes.func, 91 | useNativeDriver: PropTypes.bool, 92 | delay: PropTypes.number, 93 | }; 94 | 95 | AnimatedCircularProgress.defaultProps = { 96 | duration: 500, 97 | easing: Easing.out(Easing.ease), 98 | prefill: 0, 99 | useNativeDriver: false, 100 | delay: 0, 101 | }; 102 | -------------------------------------------------------------------------------- /src/CircularProgress.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Animated, View } from 'react-native'; 4 | import { Svg, Path, G } from 'react-native-svg'; 5 | 6 | export default class CircularProgress extends React.PureComponent { 7 | polarToCartesian(centerX, centerY, radius, angleInDegrees) { 8 | var angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180.0; 9 | return { 10 | x: centerX + radius * Math.cos(angleInRadians), 11 | y: centerY + radius * Math.sin(angleInRadians), 12 | }; 13 | } 14 | 15 | circlePath(x, y, radius, startAngle, endAngle) { 16 | var start = this.polarToCartesian(x, y, radius, endAngle * 0.9999999); 17 | var end = this.polarToCartesian(x, y, radius, startAngle); 18 | var largeArcFlag = endAngle - startAngle <= 180 ? '0' : '1'; 19 | var d = ['M', start.x, start.y, 'A', radius, radius, 0, largeArcFlag, 0, end.x, end.y]; 20 | return d.join(' '); 21 | } 22 | 23 | clampFill = fill => Math.min(100, Math.max(0, fill)); 24 | 25 | render() { 26 | const { 27 | size, 28 | width, 29 | backgroundWidth, 30 | tintColor, 31 | tintTransparency, 32 | backgroundColor, 33 | style, 34 | rotation, 35 | lineCap, 36 | fillLineCap = lineCap, 37 | arcSweepAngle, 38 | fill, 39 | children, 40 | childrenContainerStyle, 41 | padding, 42 | renderCap, 43 | dashedBackground, 44 | dashedTint 45 | } = this.props; 46 | 47 | const maxWidthCircle = backgroundWidth ? Math.max(width, backgroundWidth) : width; 48 | const sizeWithPadding = size / 2 + padding / 2; 49 | const radius = size / 2 - maxWidthCircle / 2 - padding / 2; 50 | 51 | const currentFillAngle = (arcSweepAngle * this.clampFill(fill)) / 100; 52 | const backgroundPath = this.circlePath( 53 | sizeWithPadding, 54 | sizeWithPadding, 55 | radius, 56 | tintTransparency ? 0 : currentFillAngle, 57 | arcSweepAngle 58 | ); 59 | const circlePath = this.circlePath( 60 | sizeWithPadding, 61 | sizeWithPadding, 62 | radius, 63 | 0, 64 | currentFillAngle 65 | ); 66 | const coordinate = this.polarToCartesian( 67 | sizeWithPadding, 68 | sizeWithPadding, 69 | radius, 70 | currentFillAngle 71 | ); 72 | const cap = this.props.renderCap ? this.props.renderCap({ center: coordinate }) : null; 73 | 74 | const offset = size - maxWidthCircle * 2; 75 | 76 | const localChildrenContainerStyle = { 77 | ...{ 78 | position: 'absolute', 79 | left: maxWidthCircle + padding / 2, 80 | top: maxWidthCircle + padding / 2, 81 | width: offset, 82 | height: offset, 83 | borderRadius: offset / 2, 84 | alignItems: 'center', 85 | justifyContent: 'center', 86 | overflow: 'hidden', 87 | }, 88 | ...childrenContainerStyle, 89 | } 90 | 91 | const strokeDasharrayTint = dashedTint.gap > 0 ? 92 | Object.values(dashedTint) 93 | .map(value => parseInt(value)) 94 | : null; 95 | 96 | const strokeDasharrayBackground = dashedBackground.gap > 0 ? 97 | Object.values(dashedBackground) 98 | .map(value => parseInt(value)) 99 | : null; 100 | 101 | return ( 102 | 103 | 104 | 105 | {backgroundColor && ( 106 | 114 | )} 115 | {fill > 0 && ( 116 | 124 | )} 125 | {cap} 126 | 127 | 128 | {children && {children(fill)}} 129 | 130 | ); 131 | } 132 | } 133 | 134 | CircularProgress.propTypes = { 135 | style: PropTypes.oneOfType([ 136 | PropTypes.object, 137 | PropTypes.array, 138 | ]), 139 | size: PropTypes.oneOfType([ 140 | PropTypes.number, 141 | PropTypes.instanceOf(Animated.Value), 142 | ]).isRequired, 143 | fill: PropTypes.number.isRequired, 144 | width: PropTypes.number.isRequired, 145 | backgroundWidth: PropTypes.number, 146 | tintColor: PropTypes.string, 147 | tintTransparency: PropTypes.bool, 148 | backgroundColor: PropTypes.string, 149 | rotation: PropTypes.number, 150 | lineCap: PropTypes.string, 151 | arcSweepAngle: PropTypes.number, 152 | children: PropTypes.func, 153 | childrenContainerStyle: PropTypes.object, 154 | padding: PropTypes.number, 155 | renderCap: PropTypes.func, 156 | dashedBackground: PropTypes.object, 157 | dashedTint: PropTypes.object 158 | }; 159 | 160 | CircularProgress.defaultProps = { 161 | tintColor: 'black', 162 | tintTransparency: true, 163 | rotation: 90, 164 | lineCap: 'butt', 165 | arcSweepAngle: 360, 166 | padding: 0, 167 | dashedBackground: { width: 0, gap: 0 }, 168 | dashedTint: { width: 0, gap: 0 }, 169 | }; 170 | --------------------------------------------------------------------------------