├── .husky ├── .gitignore └── pre-commit ├── .eslintignore ├── .eslintrc.json ├── demo.gif ├── example ├── assets │ ├── icon.png │ ├── splash.png │ ├── favicon.png │ └── adaptive-icon.png ├── .gitignore ├── .expo-shared │ └── assets.json ├── tsconfig.json ├── babel.config.js ├── app.json ├── package.json ├── webpack.config.js ├── metro.config.js └── App.tsx ├── .prettierrc.json ├── .gitignore ├── src ├── SpinnerProps.ts ├── index.ts ├── Pulse.tsx ├── Wave.tsx ├── Plane.tsx ├── Bounce.tsx ├── Flow.tsx ├── Grid.tsx ├── Circle.tsx ├── CircleFade.tsx ├── AnimationContainer.tsx ├── Swing.tsx ├── Chase.tsx ├── utils.ts ├── Fold.tsx └── Wander.tsx ├── tsconfig.json ├── LICENSE ├── package.json └── README.md /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": "tienphaw" 4 | } 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname $0)/_/husky.sh" 3 | 4 | yarn lint -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phamfoo/react-native-animated-spinkit/HEAD/demo.gif -------------------------------------------------------------------------------- /example/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phamfoo/react-native-animated-spinkit/HEAD/example/assets/icon.png -------------------------------------------------------------------------------- /example/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phamfoo/react-native-animated-spinkit/HEAD/example/assets/splash.png -------------------------------------------------------------------------------- /example/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phamfoo/react-native-animated-spinkit/HEAD/example/assets/favicon.png -------------------------------------------------------------------------------- /example/assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phamfoo/react-native-animated-spinkit/HEAD/example/assets/adaptive-icon.png -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "tabWidth": 2, 4 | "trailingComma": "es5", 5 | "useTabs": false, 6 | "semi": false, 7 | "quoteProps": "consistent" 8 | } 9 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/* 3 | npm-debug.* 4 | *.jks 5 | *.p8 6 | *.p12 7 | *.key 8 | *.mobileprovision 9 | *.orig.* 10 | web-build/ 11 | 12 | # macOS 13 | .DS_Store 14 | -------------------------------------------------------------------------------- /example/.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true, 3 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | 4 | # Node 5 | node_modules/ 6 | npm-debug.log 7 | yarn-debug.log 8 | yarn-error.log 9 | 10 | # Expo 11 | .expo/ 12 | 13 | # Build 14 | dist/ 15 | lib/ 16 | -------------------------------------------------------------------------------- /src/SpinnerProps.ts: -------------------------------------------------------------------------------- 1 | import { ViewProps } from 'react-native' 2 | 3 | export interface SpinnerProps extends ViewProps { 4 | size: number 5 | color: string 6 | animating: boolean 7 | hidesWhenStopped: boolean 8 | } 9 | 10 | export const defaultProps = { 11 | size: 48, 12 | color: '#000', 13 | animating: true, 14 | hidesWhenStopped: true, 15 | } 16 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "allowSyntheticDefaultImports": true, 5 | "jsx": "react-native", 6 | "lib": ["dom", "esnext"], 7 | "moduleResolution": "node", 8 | "noEmit": true, 9 | "skipLibCheck": true, 10 | "resolveJsonModule": true, 11 | "strict": true, 12 | "paths": { 13 | "react-native-animated-spinkit": ["../src/index"] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const pak = require('../package.json') 3 | 4 | module.exports = function (api) { 5 | api.cache(true) 6 | 7 | return { 8 | presets: ['babel-preset-expo'], 9 | plugins: [ 10 | [ 11 | 'module-resolver', 12 | { 13 | alias: { 14 | // For development, we want to alias the library to the source 15 | [pak.name]: path.join(__dirname, '..', pak.source), 16 | }, 17 | }, 18 | ], 19 | ], 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Plane } from './Plane' 2 | export { default as Bounce } from './Bounce' 3 | export { default as Pulse } from './Pulse' 4 | export { default as Swing } from './Swing' 5 | export { default as Chase } from './Chase' 6 | export { default as Wave } from './Wave' 7 | export { default as Flow } from './Flow' 8 | export { default as Circle } from './Circle' 9 | export { default as CircleFade } from './CircleFade' 10 | export { default as Grid } from './Grid' 11 | export { default as Fold } from './Fold' 12 | export { default as Wander } from './Wander' 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowUnreachableCode": false, 4 | "allowUnusedLabels": false, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "jsx": "react", 8 | "lib": ["esnext"], 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "noFallthroughCasesInSwitch": true, 12 | "noImplicitReturns": true, 13 | "noImplicitUseStrict": false, 14 | "noStrictGenericChecks": false, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "resolveJsonModule": true, 18 | "skipLibCheck": true, 19 | "strict": true, 20 | "target": "esnext" 21 | }, 22 | "include": ["src"] 23 | } 24 | 25 | -------------------------------------------------------------------------------- /example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "example", 4 | "slug": "example", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "splash": { 9 | "image": "./assets/splash.png", 10 | "resizeMode": "contain", 11 | "backgroundColor": "#ffffff" 12 | }, 13 | "updates": { 14 | "fallbackToCacheTimeout": 0 15 | }, 16 | "assetBundlePatterns": [ 17 | "**/*" 18 | ], 19 | "ios": { 20 | "supportsTablet": true 21 | }, 22 | "android": { 23 | "adaptiveIcon": { 24 | "foregroundImage": "./assets/adaptive-icon.png", 25 | "backgroundColor": "#FFFFFF" 26 | } 27 | }, 28 | "web": { 29 | "favicon": "./assets/favicon.png" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /example/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": "~40.0.0", 12 | "expo-status-bar": "~1.0.3", 13 | "react": "16.13.1", 14 | "react-dom": "16.13.1", 15 | "react-native": "https://github.com/expo/react-native/archive/sdk-40.0.1.tar.gz", 16 | "react-native-web": "~0.13.12" 17 | }, 18 | "devDependencies": { 19 | "@babel/core": "~7.9.0", 20 | "@expo/webpack-config": "^0.12.52", 21 | "@types/react": "~16.9.35", 22 | "@types/react-dom": "~16.9.8", 23 | "@types/react-native": "~0.63.2", 24 | "babel-plugin-module-resolver": "^4.1.0", 25 | "typescript": "~4.0.0" 26 | }, 27 | "private": true 28 | } 29 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const createExpoWebpackConfigAsync = require('@expo/webpack-config') 3 | const { resolver } = require('./metro.config') 4 | 5 | const root = path.resolve(__dirname, '..') 6 | const node_modules = path.join(__dirname, 'node_modules') 7 | 8 | module.exports = async function (env, argv) { 9 | const config = await createExpoWebpackConfigAsync(env, argv) 10 | 11 | config.module.rules.push({ 12 | test: /\.(js|jsx|ts|tsx)$/, 13 | include: path.resolve(root, 'src'), 14 | use: 'babel-loader', 15 | }) 16 | 17 | // We need to make sure that only one version is loaded for peerDependencies 18 | // So we alias them to the versions in example's node_modules 19 | Object.assign(config.resolve.alias, { 20 | ...resolver.extraNodeModules, 21 | 'react-native-web': path.join(node_modules, 'react-native-web'), 22 | }) 23 | 24 | return config 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Tien Pham 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/metro.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const blacklist = require('metro-config/src/defaults/blacklist') 3 | const escape = require('escape-string-regexp') 4 | const pak = require('../package.json') 5 | 6 | const root = path.resolve(__dirname, '..') 7 | 8 | const modules = Object.keys({ 9 | ...pak.peerDependencies, 10 | }) 11 | 12 | module.exports = { 13 | projectRoot: __dirname, 14 | watchFolders: [root], 15 | 16 | // We need to make sure that only one version is loaded for peerDependencies 17 | // So we blacklist them at the root, and alias them to the versions in example's node_modules 18 | resolver: { 19 | blacklistRE: blacklist( 20 | modules.map( 21 | (m) => 22 | new RegExp(`^${escape(path.join(root, 'node_modules', m))}\\/.*$`) 23 | ) 24 | ), 25 | 26 | extraNodeModules: modules.reduce((acc, name) => { 27 | acc[name] = path.join(__dirname, 'node_modules', name) 28 | return acc 29 | }, {}), 30 | }, 31 | 32 | transformer: { 33 | getTransformOptions: async () => ({ 34 | transform: { 35 | experimentalImportSupport: false, 36 | inlineRequires: true, 37 | }, 38 | }), 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-animated-spinkit", 3 | "description": "A pure JavaScript port of SpinKit for React Native.", 4 | "version": "1.5.2", 5 | "author": "Tien Pham", 6 | "main": "lib/commonjs/index.js", 7 | "license": "MIT", 8 | "source": "src/index.ts", 9 | "react-native": "src/index.ts", 10 | "module": "lib/module/index.js", 11 | "types": "lib/typescript/index.d.ts", 12 | "scripts": { 13 | "prepare": "bob build", 14 | "postinstall": "husky install", 15 | "prepublishOnly": "pinst --disable", 16 | "postpublish": "pinst --enable", 17 | "lint": "tsc --noEmit && eslint src --ext ts,tsx" 18 | }, 19 | "peerDependencies": { 20 | "react": "*", 21 | "react-native": "*" 22 | }, 23 | "devDependencies": { 24 | "@types/react": "^16.9.19", 25 | "@types/react-native": "0.62.13", 26 | "eslint": "^7.16.0", 27 | "eslint-config-tienphaw": "^1.4.0", 28 | "husky": "^5.0.6", 29 | "pinst": "^2.1.1", 30 | "prettier": "^2.2.1", 31 | "react": "16.13.1", 32 | "react-native": "0.63.4", 33 | "react-native-builder-bob": "^0.17.1", 34 | "typescript": "^4.1.3" 35 | }, 36 | "files": [ 37 | "src", 38 | "lib", 39 | "!**/__tests__", 40 | "!**/__fixtures__", 41 | "!**/__mocks__" 42 | ], 43 | "keywords": [ 44 | "react", 45 | "react-native", 46 | "spinkit", 47 | "expo", 48 | "loading" 49 | ], 50 | "repository": { 51 | "type": "git", 52 | "url": "https://github.com/tienphaw/react-native-animated-spinkit.git" 53 | }, 54 | "react-native-builder-bob": { 55 | "source": "src", 56 | "output": "lib", 57 | "targets": [ 58 | "commonjs", 59 | "module", 60 | "typescript" 61 | ] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Pulse.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Animated, Easing } from 'react-native' 3 | import { SpinnerProps, defaultProps } from './SpinnerProps' 4 | import AnimationContainer from './AnimationContainer' 5 | import { loop } from './utils' 6 | 7 | export default class Pulse extends React.Component { 8 | static defaultProps = defaultProps 9 | 10 | render() { 11 | const { 12 | size, 13 | color, 14 | style, 15 | animating, 16 | hidesWhenStopped, 17 | ...rest 18 | } = this.props 19 | 20 | return ( 21 | ({ 23 | pulse: (value) => ({ 24 | values: [value], 25 | animation: loop({ 26 | duration: 1200, 27 | value: value, 28 | easing: Easing.bezier(0.455, 0.03, 0.515, 0.955), 29 | }), 30 | }), 31 | })} 32 | animating={animating} 33 | > 34 | {(values) => ( 35 | 62 | )} 63 | 64 | ) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Wave.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Animated, View } from 'react-native' 3 | import { SpinnerProps, defaultProps } from './SpinnerProps' 4 | import AnimationContainer from './AnimationContainer' 5 | import { stagger } from './utils' 6 | 7 | export default class Wave extends React.Component { 8 | static defaultProps = defaultProps 9 | 10 | render() { 11 | const { 12 | size, 13 | color, 14 | style, 15 | animating, 16 | hidesWhenStopped, 17 | ...rest 18 | } = this.props 19 | 20 | return ( 21 | ({ 23 | wave: (value) => 24 | stagger(100, 5, { 25 | duration: 1200, 26 | value: value, 27 | keyframes: [0, 20, 40, 100], 28 | }), 29 | })} 30 | animating={animating} 31 | > 32 | {(values) => ( 33 | 46 | {values.wave.map((value, index) => ( 47 | 63 | ))} 64 | 65 | )} 66 | 67 | ) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Plane.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Animated } from 'react-native' 3 | import { SpinnerProps, defaultProps } from './SpinnerProps' 4 | import AnimationContainer from './AnimationContainer' 5 | import { loop } from './utils' 6 | 7 | export default class Plane extends React.Component { 8 | static defaultProps = defaultProps 9 | 10 | render() { 11 | const { 12 | size, 13 | color, 14 | style, 15 | animating, 16 | hidesWhenStopped, 17 | ...rest 18 | } = this.props 19 | 20 | return ( 21 | ({ 23 | plane: (value) => ({ 24 | values: [value], 25 | animation: loop({ 26 | duration: 1200, 27 | value: value, 28 | keyframes: [0, 50, 100], 29 | }), 30 | }), 31 | })} 32 | animating={animating} 33 | > 34 | {(values) => ( 35 | 64 | )} 65 | 66 | ) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Bounce.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Animated, View } from 'react-native' 3 | import { SpinnerProps, defaultProps } from './SpinnerProps' 4 | import AnimationContainer from './AnimationContainer' 5 | import { stagger } from './utils' 6 | 7 | export default class Bounce extends React.Component { 8 | static defaultProps = defaultProps 9 | 10 | render() { 11 | const { 12 | size, 13 | color, 14 | style, 15 | animating, 16 | hidesWhenStopped, 17 | ...rest 18 | } = this.props 19 | const circleStyle = { 20 | position: 'absolute', 21 | width: size, 22 | height: size, 23 | backgroundColor: color, 24 | borderRadius: size / 2, 25 | opacity: 0.6, 26 | } 27 | 28 | return ( 29 | ({ 31 | bounce: (value) => 32 | stagger(1000, 2, { 33 | duration: 2000, 34 | value: value, 35 | keyframes: [0, 45, 55, 100], 36 | }), 37 | })} 38 | animating={animating} 39 | > 40 | {(values) => ( 41 | 52 | {values.bounce.map((value, index) => ( 53 | 69 | ))} 70 | 71 | )} 72 | 73 | ) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Flow.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Animated, Easing, View } from 'react-native' 3 | import { SpinnerProps, defaultProps } from './SpinnerProps' 4 | import AnimationContainer from './AnimationContainer' 5 | import { stagger } from './utils' 6 | 7 | export default class Flow extends React.Component { 8 | static defaultProps = defaultProps 9 | 10 | render() { 11 | const { 12 | size, 13 | color, 14 | style, 15 | animating, 16 | hidesWhenStopped, 17 | ...rest 18 | } = this.props 19 | 20 | return ( 21 | ({ 23 | flow: (value) => 24 | stagger(150, 3, { 25 | duration: 1400, 26 | value: value, 27 | easing: Easing.bezier(0.455, 0.03, 0.515, 0.955), 28 | keyframes: [0, 40, 80, 100], 29 | }), 30 | })} 31 | animating={animating} 32 | > 33 | {(values) => ( 34 | 47 | {values.flow.map((value, index) => ( 48 | 65 | ))} 66 | 67 | )} 68 | 69 | ) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Grid.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Animated, View } from 'react-native' 3 | import { SpinnerProps, defaultProps } from './SpinnerProps' 4 | import AnimationContainer from './AnimationContainer' 5 | import { stagger } from './utils' 6 | 7 | const values = [2, 3, 4, 1, 2, 3, 0, 1, 2] 8 | export default class Grid extends React.Component { 9 | static defaultProps = defaultProps 10 | 11 | render() { 12 | const { 13 | size, 14 | color, 15 | style, 16 | animating, 17 | hidesWhenStopped, 18 | ...rest 19 | } = this.props 20 | 21 | return ( 22 | ({ 24 | grid: (value) => 25 | stagger(100, 5, { 26 | duration: 1300, 27 | value: value, 28 | keyframes: [0, 35, 70, 100], 29 | }), 30 | })} 31 | animating={animating} 32 | > 33 | {(interpolations) => ( 34 | 47 | {values 48 | .map((value) => interpolations.grid[value]) 49 | .map((value, index) => ( 50 | 66 | ))} 67 | 68 | )} 69 | 70 | ) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Circle.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Animated, View } from 'react-native' 3 | import { SpinnerProps, defaultProps } from './SpinnerProps' 4 | import AnimationContainer from './AnimationContainer' 5 | import { stagger } from './utils' 6 | 7 | export default class Circle extends React.Component { 8 | static defaultProps = defaultProps 9 | 10 | render() { 11 | const { 12 | size, 13 | color, 14 | style, 15 | animating, 16 | hidesWhenStopped, 17 | ...rest 18 | } = this.props 19 | const circleStyle = { 20 | position: 'absolute', 21 | width: size * 0.15, 22 | height: size * 0.15, 23 | backgroundColor: color, 24 | borderRadius: (size * 0.15) / 2, 25 | } 26 | 27 | return ( 28 | ({ 30 | circle: (value) => 31 | stagger(100, 12, { 32 | duration: 1200, 33 | value: value, 34 | keyframes: [0, 40, 80, 100], 35 | }), 36 | })} 37 | animating={animating} 38 | > 39 | {(values) => ( 40 | 53 | {values.circle.map((value, index) => ( 54 | 74 | ))} 75 | 76 | )} 77 | 78 | ) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Native Animated SpinKit 2 | 3 | [![Stable Release](https://img.shields.io/npm/v/react-native-animated-spinkit.svg)](https://npm.im/react-native-animated-spinkit) [![license](https://badgen.now.sh/badge/license/MIT)](./LICENSE) 4 | 5 | A pure JavaScript port of [SpinKit](https://github.com/tobiasahlin/SpinKit) for React Native. 6 | 7 | ![](demo.gif) 8 | 9 | ## Why Another Port? 10 | 11 | The previous port of [SpinKit](https://github.com/tobiasahlin/SpinKit) for React Native ([react-native-spinkit](https://github.com/maxs15/react-native-spinkit)) is a native module so it requires extra native dependencies and can't be used in [Expo](https://expo.io) projects without ejecting. 12 | 13 | This library is a pure JavaScript port of SpinKit implemented with the [Animated](https://facebook.github.io/react-native/docs/animated) API, which means you can use it in any React Native project and the spinners will look identical on Android and iOS. 14 | 15 | ## Installation 16 | 17 | ```sh 18 | yarn add react-native-animated-spinkit 19 | ``` 20 | 21 | or 22 | 23 | ```sh 24 | npm install react-native-animated-spinkit 25 | ``` 26 | 27 | ## Usage 28 | 29 | ```js 30 | import { Plane } from 'react-native-animated-spinkit' 31 | 32 | function App() { 33 | return ( 34 | 35 | 36 | 37 | ) 38 | } 39 | ``` 40 | 41 | ## Props 42 | 43 | Inherits [View Props](https://facebook.github.io/react-native/docs/view#props) 44 | 45 | ### size 46 | 47 | > `number` | defaults to `48` 48 | 49 | Width and height of the spinner. 50 | 51 | ### color 52 | 53 | > `string` | defaults to `#000` 54 | 55 | Color of the spinner. 56 | 57 | ### animating 58 | 59 | > `boolean` | defaults to `true` 60 | 61 | Whether to show the indicator or hide it. 62 | 63 | ### hidesWhenStopped 64 | 65 | > `boolean` | defaults to `true` 66 | 67 | Whether the indicator should hide when not animating. 68 | 69 | ## Spinners 70 | 71 | All the spinners from [SpinKit](https://github.com/tobiasahlin/SpinKit) have been ported. 72 | 73 | - `` 74 | - `` 75 | - `` 76 | - `` 77 | - `` 78 | - `` 79 | - `` 80 | - `` 81 | - `` 82 | - `` 83 | - `` 84 | - `` 85 | 86 | ## Example 87 | 88 | To run the example project, follow these steps: 89 | 90 | - Clone the repo 91 | - Run these commands 92 | 93 | ```sh 94 | yarn 95 | cd example 96 | yarn && yarn start 97 | ``` 98 | -------------------------------------------------------------------------------- /src/CircleFade.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Animated, View } from 'react-native' 3 | import { SpinnerProps, defaultProps } from './SpinnerProps' 4 | import AnimationContainer from './AnimationContainer' 5 | import { stagger } from './utils' 6 | 7 | export default class CircleFade extends React.Component { 8 | static defaultProps = defaultProps 9 | 10 | render() { 11 | const { 12 | size, 13 | color, 14 | style, 15 | animating, 16 | hidesWhenStopped, 17 | ...rest 18 | } = this.props 19 | const circleStyle = { 20 | position: 'absolute', 21 | width: size * 0.15, 22 | height: size * 0.15, 23 | backgroundColor: color, 24 | borderRadius: (size * 0.15) / 2, 25 | } 26 | 27 | return ( 28 | ({ 30 | circleFade: (value) => 31 | stagger(100, 12, { 32 | duration: 1200, 33 | value: value, 34 | keyframes: [0, 39, 40, 100], 35 | }), 36 | })} 37 | animating={animating} 38 | > 39 | {(values) => ( 40 | 53 | {values.circleFade.map((value, index) => ( 54 | 78 | ))} 79 | 80 | )} 81 | 82 | ) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/AnimationContainer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Animated } from 'react-native' 3 | 4 | interface AnimationNode { 5 | animation: Animated.CompositeAnimation 6 | values: Animated.AnimatedInterpolation[] 7 | } 8 | 9 | export interface Props { 10 | initAnimation: () => Record AnimationNode> 11 | children: ( 12 | interpolationsByKey: Record 13 | ) => React.ReactNode 14 | animating: boolean 15 | } 16 | 17 | export default class AnimationContainer< 18 | T extends string 19 | > extends React.Component> { 20 | animation: Animated.CompositeAnimation 21 | animatedValuesByKey: Record = {} as Record< 22 | T, 23 | Animated.Value 24 | > 25 | interpolationsByKey: Record< 26 | T, 27 | Animated.AnimatedInterpolation[] 28 | > = {} as Record 29 | 30 | static defaultProps = { 31 | animating: true, 32 | } 33 | 34 | constructor(props: Props) { 35 | super(props) 36 | const { initAnimation } = props 37 | 38 | const animationInitializersByKey = initAnimation() 39 | const animations: Animated.CompositeAnimation[] = [] 40 | 41 | for (const key in animationInitializersByKey) { 42 | const animationInitializer = animationInitializersByKey[key] 43 | const animationValue = new Animated.Value(0) 44 | this.animatedValuesByKey[key] = animationValue 45 | const { animation, values } = animationInitializer(animationValue) 46 | animations.push(animation) 47 | this.interpolationsByKey[key] = values 48 | } 49 | 50 | if (animations.length === 1) { 51 | this.animation = animations[0] 52 | } else { 53 | this.animation = Animated.parallel(animations) 54 | } 55 | } 56 | 57 | componentDidMount() { 58 | if (this.props.animating) { 59 | this.startAnimation() 60 | } 61 | } 62 | 63 | componentDidUpdate(prevProps: Props) { 64 | const { animating } = this.props 65 | 66 | if (animating !== prevProps.animating) { 67 | if (animating) { 68 | this.startAnimation() 69 | } else { 70 | this.stopAnimation() 71 | } 72 | } 73 | } 74 | 75 | startAnimation = () => { 76 | this.animation.start() 77 | } 78 | 79 | stopAnimation = () => { 80 | this.animation.reset() 81 | 82 | for (const key in this.animatedValuesByKey) { 83 | this.animatedValuesByKey[key].setValue(0) 84 | } 85 | } 86 | 87 | componentWillUnmount() { 88 | this.animation.stop() 89 | } 90 | 91 | render() { 92 | const { children } = this.props 93 | return children ? children(this.interpolationsByKey) : null 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Swing.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Animated, Easing } from 'react-native' 3 | import { SpinnerProps, defaultProps } from './SpinnerProps' 4 | import AnimationContainer from './AnimationContainer' 5 | import { loop } from './utils' 6 | 7 | export default class Swing extends React.Component { 8 | static defaultProps = defaultProps 9 | 10 | render() { 11 | const { 12 | size, 13 | color, 14 | style, 15 | animating, 16 | hidesWhenStopped, 17 | ...rest 18 | } = this.props 19 | const circleStyle = { 20 | width: size * 0.45, 21 | height: size * 0.45, 22 | backgroundColor: color, 23 | borderRadius: (size * 0.45) / 2, 24 | } 25 | 26 | return ( 27 | ({ 29 | swing: (value) => ({ 30 | values: [value], 31 | animation: loop({ 32 | duration: 1800, 33 | value: value, 34 | easing: Easing.linear, 35 | keyframes: [0, 100], 36 | }), 37 | }), 38 | swingDot: (value) => ({ 39 | values: [value], 40 | animation: loop({ 41 | duration: 2000, 42 | value: value, 43 | keyframes: [0, 50, 100], 44 | }), 45 | }), 46 | })} 47 | animating={animating} 48 | > 49 | {(values) => ( 50 | 71 | 86 | 101 | 102 | )} 103 | 104 | ) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Chase.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Animated, Easing } from 'react-native' 3 | import { SpinnerProps, defaultProps } from './SpinnerProps' 4 | import AnimationContainer from './AnimationContainer' 5 | import { loop, stagger } from './utils' 6 | 7 | export default class Chase extends React.Component { 8 | static defaultProps = defaultProps 9 | 10 | render() { 11 | const { 12 | size, 13 | color, 14 | style, 15 | animating, 16 | hidesWhenStopped, 17 | ...rest 18 | } = this.props 19 | const circleStyle = { 20 | position: 'absolute', 21 | width: size / 4, 22 | height: size / 4, 23 | backgroundColor: color, 24 | borderRadius: size / 8, 25 | } 26 | 27 | return ( 28 | ({ 30 | chase: (value) => ({ 31 | values: [value], 32 | animation: loop({ 33 | duration: 2500, 34 | easing: Easing.linear, 35 | value: value, 36 | }), 37 | }), 38 | chaseDot: (value) => 39 | stagger(100, 6, { 40 | duration: 2000, 41 | value: value, 42 | keyframes: [0, 80, 100], 43 | }), 44 | chaseDotBefore: (value) => 45 | stagger(100, 6, { 46 | duration: 2000, 47 | value: value, 48 | keyframes: [0, 50, 100], 49 | }), 50 | })} 51 | animating={animating} 52 | > 53 | {(values) => ( 54 | 75 | {values.chaseDot.map((value, index) => ( 76 | 99 | ))} 100 | 101 | )} 102 | 103 | ) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /example/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useReducer } from 'react' 2 | import { 3 | View, 4 | StatusBar, 5 | Text, 6 | StyleSheet, 7 | TouchableOpacity, 8 | } from 'react-native' 9 | import { 10 | Plane, 11 | Chase, 12 | Bounce, 13 | Wave, 14 | Wander, 15 | Pulse, 16 | Flow, 17 | Circle, 18 | Grid, 19 | CircleFade, 20 | Fold, 21 | Swing, 22 | } from 'react-native-animated-spinkit' 23 | 24 | const spinners = [ 25 | { component: Plane, backgroundColor: '#d35400' }, 26 | { component: Chase, backgroundColor: '#2c3e50' }, 27 | { component: Bounce, backgroundColor: '#1abc9c' }, 28 | { component: Wave, backgroundColor: '#2980b9' }, 29 | { component: Wander, backgroundColor: '#7f8c8d' }, 30 | { component: Pulse, backgroundColor: '#ffcb65' }, 31 | { component: Swing, backgroundColor: '#d35400' }, 32 | { component: Flow, backgroundColor: '#27ae60' }, 33 | { component: Circle, backgroundColor: '#d35400' }, 34 | { component: Grid, backgroundColor: '#2c3e50' }, 35 | { component: CircleFade, backgroundColor: '#1abc9c' }, 36 | { component: Fold, backgroundColor: '#2980b9' }, 37 | ] 38 | 39 | function reducer(state, action) { 40 | switch (action.type) { 41 | case 'toggle_loading': 42 | return { ...state, [action.spinnerIndex]: !state[action.spinnerIndex] } 43 | default: 44 | return state 45 | } 46 | } 47 | 48 | const initialState = spinners.reduce( 49 | (acc, _, index) => ({ ...acc, [index]: true }), 50 | {} 51 | ) 52 | 53 | export default function App() { 54 | const [state, dispatch] = useReducer(reducer, initialState) 55 | 56 | return ( 57 | 58 | 59 | {Array(Math.ceil(spinners.length / 3)) 60 | .fill(null) 61 | .map((_, rowIndex) => ( 62 | 63 | {spinners 64 | .slice(rowIndex * 3, rowIndex * 3 + 3) 65 | .map((spinner, index) => { 66 | const Spinner = spinner.component 67 | const spinnerIndex = rowIndex * 3 + index 68 | return ( 69 | { 78 | dispatch({ 79 | type: 'toggle_loading', 80 | spinnerIndex: spinnerIndex, 81 | }) 82 | }} 83 | > 84 | 85 | 86 | {`<${spinner.component.name} />`} 87 | 88 | 89 | ) 90 | })} 91 | 92 | ))} 93 | 94 | ) 95 | } 96 | 97 | const styles = StyleSheet.create({ 98 | container: { 99 | flex: 1, 100 | }, 101 | componentLabel: { 102 | position: 'absolute', 103 | bottom: 14, 104 | color: '#FFF', 105 | fontWeight: 'bold', 106 | opacity: 0.7, 107 | }, 108 | row: { 109 | flexDirection: 'row', 110 | flex: 1, 111 | }, 112 | cell: { 113 | flex: 1, 114 | alignItems: 'center', 115 | justifyContent: 'center', 116 | }, 117 | }) 118 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { EasingFunction, Easing, Animated, Platform } from 'react-native' 2 | 3 | function createKeyframeEasingFunction( 4 | keyframes: number[], 5 | easing: EasingFunction 6 | ) { 7 | return (t: number) => { 8 | for ( 9 | let keyframeIndex = 1; 10 | keyframeIndex < keyframes.length; 11 | keyframeIndex++ 12 | ) { 13 | if (t < keyframes[keyframeIndex] / 100) { 14 | const prev = keyframes[keyframeIndex - 1] / 100 15 | const current = 16 | (keyframes[keyframeIndex] - keyframes[keyframeIndex - 1]) / 100 17 | 18 | return prev + easing((t - prev) / current) * current 19 | } 20 | } 21 | return t 22 | } 23 | } 24 | 25 | interface AnimationConfig { 26 | duration: number 27 | value: Animated.Value 28 | keyframes?: number[] 29 | toValue?: number 30 | easing?: EasingFunction 31 | } 32 | 33 | function loop({ 34 | duration, 35 | value, 36 | keyframes = [0, 100], 37 | easing = Easing.bezier(0.42, 0.0, 0.58, 1.0), 38 | toValue = 100, 39 | }: AnimationConfig) { 40 | const timing = Animated.timing(value, { 41 | duration: duration, 42 | easing: createKeyframeEasingFunction(keyframes, easing), 43 | toValue: toValue, 44 | useNativeDriver: Platform.OS !== 'web', 45 | }) 46 | 47 | return Animated.loop(timing) 48 | } 49 | 50 | function stagger( 51 | time: number, 52 | amount: number, 53 | animationConfig: AnimationConfig 54 | ) { 55 | const { 56 | duration, 57 | value, 58 | keyframes = [0, 100], 59 | easing = Easing.bezier(0.42, 0.0, 0.58, 1.0), 60 | toValue = 100, 61 | } = animationConfig 62 | const easingFunction = createKeyframeEasingFunction(keyframes, easing) 63 | 64 | if (Platform.OS === 'web') { 65 | const values = new Array(amount) 66 | .fill(null) 67 | .map((_) => new Animated.Value(0)) 68 | 69 | const animations = values.map((value) => 70 | loop({ 71 | value, 72 | duration, 73 | easing, 74 | toValue, 75 | keyframes, 76 | }) 77 | ) 78 | 79 | const animation = Animated.stagger(time, animations) 80 | 81 | return { animation, values } 82 | } 83 | 84 | const timing = Animated.timing(value, { 85 | duration: duration, 86 | easing: easingFunction, 87 | toValue: toValue, 88 | useNativeDriver: true, 89 | }) 90 | 91 | const animation = Animated.loop(timing) 92 | 93 | // React Native only does 60fps 94 | // https://github.com/facebook/react-native/blob/d3980dceab90b118cc7f69696967aa7f8d4388d9/Libraries/Animated/src/animations/TimingAnimation.js#L78 95 | const frameDuration = 1000.0 / 60.0 96 | const inputRange: number[] = [] 97 | const numFrames = Math.round(animationConfig.duration / frameDuration) 98 | 99 | for (let frame = 0; frame < numFrames; frame++) { 100 | inputRange.push(easingFunction(frame / numFrames) * 100) 101 | } 102 | 103 | const values = [] 104 | for (let index = amount - 1; index >= 0; index--) { 105 | const delayedFrames = Math.round( 106 | ((index * time) / animationConfig.duration) * numFrames 107 | ) 108 | const outputRange = inputRange 109 | .slice(delayedFrames) 110 | .concat(inputRange.slice(0, delayedFrames)) 111 | 112 | const value = 113 | index === 0 114 | ? animationConfig.value 115 | : animationConfig.value.interpolate({ inputRange, outputRange }) 116 | values.push(value) 117 | } 118 | 119 | return { animation, values } 120 | } 121 | 122 | export { loop, stagger, AnimationConfig } 123 | -------------------------------------------------------------------------------- /src/Fold.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Animated, Easing, View } from 'react-native' 3 | import { SpinnerProps, defaultProps } from './SpinnerProps' 4 | import AnimationContainer from './AnimationContainer' 5 | import { stagger } from './utils' 6 | 7 | export default class Fold extends React.Component { 8 | static defaultProps = defaultProps 9 | 10 | render() { 11 | const { 12 | size, 13 | color, 14 | style, 15 | animating, 16 | hidesWhenStopped, 17 | ...rest 18 | } = this.props 19 | 20 | return ( 21 | ({ 23 | fold: (value) => 24 | stagger(300, 4, { 25 | duration: 2400, 26 | value: value, 27 | keyframes: [0, 10, 25, 75, 90, 100], 28 | easing: Easing.linear, 29 | }), 30 | })} 31 | animating={animating} 32 | > 33 | {(values) => ( 34 | 52 | {values.fold.map((value, index) => ( 53 | 107 | ))} 108 | 109 | )} 110 | 111 | ) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Wander.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Animated, View } from 'react-native' 3 | import { SpinnerProps, defaultProps } from './SpinnerProps' 4 | import AnimationContainer from './AnimationContainer' 5 | import { loop } from './utils' 6 | 7 | export default class Wander extends React.Component { 8 | static defaultProps = defaultProps 9 | 10 | render() { 11 | const { 12 | size, 13 | color, 14 | style, 15 | animating, 16 | hidesWhenStopped, 17 | ...rest 18 | } = this.props 19 | const wanderDistance = size * 0.75 20 | 21 | return ( 22 | ({ 24 | wander: (value) => ({ 25 | values: [value], 26 | animation: loop({ 27 | duration: 2000, 28 | value: value, 29 | keyframes: [0, 25, 50, 75, 100], 30 | }), 31 | }), 32 | })} 33 | animating={animating} 34 | > 35 | {(values) => ( 36 | 47 | {Array(3) 48 | .fill(null) 49 | .map((_, index) => ( 50 | 63 | 118 | 119 | ))} 120 | 121 | )} 122 | 123 | ) 124 | } 125 | } 126 | --------------------------------------------------------------------------------