├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── assets └── showcase.gif ├── babel.config.js ├── examples ├── README.md └── expo-app │ ├── App.tsx │ ├── app.json │ ├── assets │ ├── adaptive-icon.png │ ├── favicon.png │ ├── icon.png │ └── splash.png │ ├── babel.config.js │ ├── package.json │ └── tsconfig.json ├── package.json ├── src └── index.tsx └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | package-lock.json 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | assets/ 2 | examples/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Max Kalashnikoff 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🌈 SpringLoader [![npm version](https://badge.fury.io/js/spring-loader.svg)](https://badge.fury.io/js/spring-loader) 2 | 3 | Gradient spring animated loading activity indicator component for React Native. 4 | 5 | The animation is created in pure [reanimated2](https://docs.swmansion.com/react-native-reanimated/) and [SVG](https://developer.mozilla.org/en-US/docs/Web/SVG). 6 | 7 | It's lightweight, made in TypeScript, and can be used in the Splash screen (unlike Lottie and Rove animations). 8 | 9 | ## Showcase 10 | 11 | ![alt Showcase](https://github.com/geekbrother/SpringLoader/blob/main/assets/showcase.gif) 12 | 13 | ## What's included: 14 | 15 | - Spring animation; 16 | - Wave animation; 17 | - Array of the rgb colors for the linear gradient as a prop; 18 | - Animation of poping and hiding when the loading state changes. 19 | 20 | ## How to use 21 | 22 | The package uses TypeScript and provides types when importing. 23 | 24 | Add package to the React Native project by `npm i spring-loader`. 25 | 26 | Import the `SpringLoader` component `import { AnimationTypes, SpringLoader } from 'spring-loader';`. 27 | 28 | Find a full usage example in [examples/expo-app/App.tsx](https://github.com/geekbrother/SpringLoader/blob/main/examples/expo-app/App.tsx). 29 | 30 | ## Example app 31 | 32 | There is an `Expo` example app with the showcase in the `examples/expo-app` folder. 33 | 34 | ## Todo 35 | 36 | Implement a [Skia](https://github.com/Shopify/react-native-skia) version 37 | -------------------------------------------------------------------------------- /assets/showcase.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekbrother/SpringLoader/d4fda4d7cfe279259b96cc1381a3724a987afa02/assets/showcase.gif -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | plugins: ['react-native-reanimated/plugin'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Example Applications 2 | 3 | You can find an [Expo](https://expo.dev) application with the example usage of the activity loader 4 | in an `expo-app` folder. 5 | 6 | To start the application just run `npm install` and `npm run ios`, `npm run android` or 7 | `npm run web`. 8 | -------------------------------------------------------------------------------- /examples/expo-app/App.tsx: -------------------------------------------------------------------------------- 1 | import { StatusBar } from 'expo-status-bar'; 2 | import { StyleSheet, Text, View, Switch } from 'react-native'; 3 | import { useState} from "react"; 4 | import { AnimationTypes, SpringLoader } from 'spring-loader'; 5 | 6 | export default function App() { 7 | const [isLoading, setIsLoading] = useState(true); 8 | const [isAnimationSpring, setIsAnimationSpring] = useState(true); 9 | const gradientsArray: Array = [ 10 | '#00ADD3', 11 | '#D4B900', 12 | '#E68244', 13 | '#C95FA2', 14 | '#FF5F57', 15 | '#3D4BF7', 16 | '#34B3EA' 17 | ]; 18 | 19 | return ( 20 | <> 21 | 22 | 23 | 33 | 34 | 35 | 36 | 37 | Wave 38 | 39 | 40 | setIsAnimationSpring(!isAnimationSpring) } 45 | /> 46 | 47 | 48 | Spring 49 | 50 | 51 | 52 | 53 | 54 | 55 | Loading 56 | 57 | 58 | setIsLoading(!isLoading) } 61 | /> 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | ); 71 | } 72 | 73 | const styles = StyleSheet.create({ 74 | container: { 75 | padding: 50, 76 | flex: 1, 77 | flexDirection: 'column', 78 | backgroundColor: '#fff', 79 | alignItems: 'center', 80 | justifyContent: 'center', 81 | }, 82 | }); 83 | -------------------------------------------------------------------------------- /examples/expo-app/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "skia-loader", 4 | "slug": "skia-loader", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "userInterfaceStyle": "light", 9 | "splash": { 10 | "image": "./assets/splash.png", 11 | "resizeMode": "contain", 12 | "backgroundColor": "#ffffff" 13 | }, 14 | "assetBundlePatterns": [ 15 | "**/*" 16 | ], 17 | "ios": { 18 | "supportsTablet": true 19 | }, 20 | "android": { 21 | "adaptiveIcon": { 22 | "foregroundImage": "./assets/adaptive-icon.png", 23 | "backgroundColor": "#ffffff" 24 | } 25 | }, 26 | "web": { 27 | "favicon": "./assets/favicon.png" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/expo-app/assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekbrother/SpringLoader/d4fda4d7cfe279259b96cc1381a3724a987afa02/examples/expo-app/assets/adaptive-icon.png -------------------------------------------------------------------------------- /examples/expo-app/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekbrother/SpringLoader/d4fda4d7cfe279259b96cc1381a3724a987afa02/examples/expo-app/assets/favicon.png -------------------------------------------------------------------------------- /examples/expo-app/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekbrother/SpringLoader/d4fda4d7cfe279259b96cc1381a3724a987afa02/examples/expo-app/assets/icon.png -------------------------------------------------------------------------------- /examples/expo-app/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekbrother/SpringLoader/d4fda4d7cfe279259b96cc1381a3724a987afa02/examples/expo-app/assets/splash.png -------------------------------------------------------------------------------- /examples/expo-app/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | plugins: ['react-native-reanimated/plugin'], 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /examples/expo-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "skia-loader", 3 | "version": "1.0.0", 4 | "main": "node_modules/expo/AppEntry.js", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web" 10 | }, 11 | "dependencies": { 12 | "@shopify/react-native-skia": "0.1.172", 13 | "expo": "~48.0.10", 14 | "expo-status-bar": "~1.4.4", 15 | "react": "18.2.0", 16 | "react-native": "0.71.6", 17 | "react-native-reanimated": "~2.14.4", 18 | "react-native-svg": "13.4.0", 19 | "react-native-svg-path-gradient": "^0.4.0", 20 | "spring-loader": "^1.0.0" 21 | }, 22 | "devDependencies": { 23 | "@babel/core": "^7.20.0", 24 | "@types/react": "~18.0.14", 25 | "typescript": "^4.9.4" 26 | }, 27 | "private": true 28 | } 29 | -------------------------------------------------------------------------------- /examples/expo-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spring-loader", 3 | "version": "1.0.2", 4 | "description": "Gradient spring animated loading activity indicator component for React Native", 5 | "main": "src/index.tsx", 6 | "scripts": { 7 | "build": "tsc" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/geekbrother/SpringLoader.git" 12 | }, 13 | "keywords": [ 14 | "activity", 15 | "indicator", 16 | "loader", 17 | "activity", 18 | "react", 19 | "native", 20 | "reanimated", 21 | "svg" 22 | ], 23 | "author": 24 | { 25 | "name" : "Max Kalashnikoff", 26 | "email" : "geekmaks@gmail.com", 27 | "url" : "https://github.com/geekbrother" 28 | }, 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/geekbrother/SpringLoader/issues" 32 | }, 33 | "homepage": "https://github.com/geekbrother/SpringLoader#readme", 34 | "devDependencies": { 35 | "@babel/cli": "^7.21.0", 36 | "@babel/core": "^7.21.4", 37 | "@babel/preset-env": "^7.21.4", 38 | "@babel/preset-typescript": "^7.21.4", 39 | "@types/react": "~18.0.14", 40 | "typescript": "^5.0.4" 41 | }, 42 | "dependencies": { 43 | "react": "^18.2.0", 44 | "react-native-reanimated": "^2", 45 | "react-native-svg": "^13.9.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import Svg, { Path, LinearGradient, Stop, Defs } from 'react-native-svg'; 3 | import Animated, { 4 | useSharedValue, 5 | useAnimatedProps, 6 | withSpring, 7 | withRepeat, 8 | withTiming, 9 | } from 'react-native-reanimated'; 10 | import React from "react"; 11 | 12 | export enum AnimationTypes { 13 | Spring, 14 | Wave 15 | } 16 | 17 | export type SpringLoaderProps = { 18 | loading: boolean, 19 | width: number, 20 | height: number, 21 | strokeWidth: number, 22 | animationType: AnimationTypes, 23 | gradientsArray: Array, 24 | showHideDuration?: number, 25 | paddings?: number, 26 | springParams?: { 27 | mass?: number, 28 | stiffness?: number, 29 | restSpeedThreshold?: number, 30 | } 31 | waveParams?: { 32 | duration?: number, 33 | }, 34 | } 35 | 36 | export const SpringLoader = ({ 37 | loading, 38 | width = 300, 39 | height = 100, 40 | strokeWidth = 30, 41 | animationType, 42 | gradientsArray, 43 | showHideDuration = 200, 44 | paddings = strokeWidth, 45 | springParams = { 46 | mass: 0.9, 47 | stiffness: 200, 48 | restSpeedThreshold: 100 49 | }, 50 | waveParams = { 51 | duration: 500, 52 | } 53 | 54 | }: SpringLoaderProps) => { 55 | const secondDotPosition = useSharedValue(height - paddings); 56 | const currentStrokeWidth = useSharedValue(0); 57 | const AnimatedPath = Animated.createAnimatedComponent(Path); 58 | 59 | function startSpring() { 60 | secondDotPosition.value = height - paddings; 61 | secondDotPosition.value = withRepeat(withSpring(paddings, springParams), 0, true); 62 | currentStrokeWidth.value = withTiming(strokeWidth, {duration: showHideDuration}); 63 | } 64 | 65 | function startWave() { 66 | secondDotPosition.value = height - paddings; 67 | secondDotPosition.value = withRepeat(withTiming(paddings, waveParams), 0, true); 68 | currentStrokeWidth.value = withTiming(strokeWidth, {duration: showHideDuration}); 69 | } 70 | 71 | function stop() { 72 | currentStrokeWidth.value = withTiming(0, {duration: showHideDuration}); 73 | } 74 | 75 | useEffect(() => { 76 | if (loading){ 77 | if (animationType == AnimationTypes.Spring){ 78 | startSpring(); 79 | }else{ 80 | startWave(); 81 | } 82 | }else{ 83 | stop(); 84 | } 85 | }); 86 | 87 | const animatedProps = useAnimatedProps(() => { 88 | const svgPath = ` 89 | M ${ paddings }, ${ height / 2 } 90 | Q ${ width / 4 } ${ secondDotPosition.value }, ${ width / 2 } ${ height / 2 }, 91 | T ${ width - paddings } ${ height / 2 } 92 | `; 93 | return { 94 | d: svgPath, 95 | strokeWidth: currentStrokeWidth.value, 96 | }; 97 | }); 98 | 99 | let i:number = 0; 100 | return ( 101 | 102 | 103 | 104 | {gradientsArray.map((gradient) => { 105 | const offset = ((100 / gradientsArray.length) * i) + "%"; 106 | i++; 107 | return (); 108 | })} 109 | 110 | 111 | 117 | 118 | ); 119 | }; 120 | 121 | export default SpringLoader; 122 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "allowSyntheticDefaultImports": true, 5 | "esModuleInterop": true, 6 | "isolatedModules": true, 7 | "jsx": "react", 8 | "lib": ["es6"], 9 | "moduleResolution": "node", 10 | "noEmit": true, 11 | "strict": true, 12 | "target": "esnext" 13 | }, 14 | "exclude": [ 15 | "node_modules", 16 | "babel.config.js" 17 | ], 18 | "include": ["src"] 19 | } 20 | --------------------------------------------------------------------------------