├── .gitignore ├── Example.gif ├── LICENSE ├── README.md ├── package-lock.json ├── package.json └── src ├── index.js └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | -------------------------------------------------------------------------------- /Example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EricPKerr/react-native-animated-styles/ecc1d84522bd4022701de135a7a2b4833501c709/Example.gif -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Eric Kerr 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 | # react-native-animated-styles 2 | 3 | Easily animate react-native components between two styles. Similar concept to adding and removing a CSS class to animate HTML Elements for the web. 4 | 5 | # Install 6 | 7 | `npm install --save react-native-animated-styles` 8 | 9 | # Properties 10 | 11 | ``` 12 | 20 | ``` 21 | 22 | # Example Usage 23 | 24 | ``` 25 | import AnimatedStyles from 'react-native-animated-styles'; 26 | import { useEffect, useState } from 'react'; 27 | 28 | const MyComponent = () => { 29 | const [ active, setActive ] = useState(true); 30 | 31 | useEffect(() => { 32 | setInterval(() => { 33 | setActive((active) => !active); 34 | }, 3000); 35 | }, []); 36 | 37 | return ( 38 | 39 | 44 | 45 | ) 46 | }; 47 | 48 | const styles = StyleSheet.create({ 49 | wrap: { 50 | flex: 1, 51 | alignItems: 'center', 52 | justifyContent: 'center' 53 | }, 54 | 55 | box: { 56 | backgroundColor: 'rgb(73, 0, 9)', // normalizes colors 57 | width: 80, 58 | height: 80 59 | }, 60 | 61 | boxActive: { 62 | width: 100, 63 | height: 100, 64 | backgroundColor: '#AC0E28', 65 | transform: [ 66 | { rotate: '45deg' } // uses default values for transform properties 67 | ] 68 | } 69 | }); 70 | ``` 71 | 72 | ![Example](https://github.com/EricPKerr/react-native-animated-styles/blob/master/Example.gif?raw=true) 73 | 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-animated-styles", 3 | "version": "0.9.0", 4 | "description": "Easily animate components between two styles", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/ericpkerr/react-native-animated-styles.git" 12 | }, 13 | "keywords": [ 14 | "react-native", 15 | "animation", 16 | "animate-styles", 17 | "react", 18 | "mobile", 19 | "react-component", 20 | "react-native-component" 21 | ], 22 | "author": { 23 | "name": "Eric Kerr", 24 | "email": "EricPKerr@gmail.com" 25 | }, 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/ericpkerr/react-native-animated-styles/issues" 29 | }, 30 | "homepage": "https://github.com/ericpkerr/react-native-animated-styles#readme", 31 | "devDependencies": { 32 | "babel": "^6.23.0", 33 | "babel-eslint": "^10.1.0", 34 | "babel-jest": "^26.6.3", 35 | "babel-preset-react-native": "^4.0.1", 36 | "jest": "26.6.3", 37 | "jest-cli": "26.6.3", 38 | "jest-react-native": "18.0.0", 39 | "react": "*", 40 | "react-native": "^0.64.1", 41 | "react-test-renderer": "17.0.1" 42 | }, 43 | "dependencies": { 44 | "prop-types": "^15.7.2", 45 | "tinycolor2": "^1.4.2" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, { PureComponent } from 'react'; 4 | import PropTypes from 'prop-types'; 5 | 6 | import { 7 | Animated, 8 | ViewPropTypes 9 | } from 'react-native'; 10 | 11 | import { 12 | flattenStyle, 13 | getDefaultStyleValue, 14 | wrapTransforms 15 | } from './utils'; 16 | 17 | import tinycolor from 'tinycolor2'; 18 | 19 | function createComponent(WrappedComponent) { 20 | class AnimatedStylesComponent extends PureComponent { 21 | static propTypes = { 22 | style: ViewPropTypes.style, 23 | animatedStyle: ViewPropTypes.style, 24 | duration: PropTypes.number, 25 | animateInitial: PropTypes.bool, 26 | active: PropTypes.bool, 27 | useNativeDriver: PropTypes.bool 28 | }; 29 | 30 | static defaultProps = { 31 | duration: 500, 32 | animateInitial: false, 33 | active: false, 34 | useNativeDriver: false 35 | }; 36 | 37 | constructor(props) { 38 | super(props); 39 | 40 | this.state = { 41 | defaultStyle: this.getDefaultStyle(), 42 | active: props.active, 43 | animatedValue: new Animated.Value(props.active && !props.animateInitial ? 1 : 0) 44 | } 45 | 46 | this.state.animatedStyle = this.getAnimatedStyle(); 47 | } 48 | 49 | componentDidMount() { 50 | if(this.props.animateInitial) this.animate(); 51 | } 52 | 53 | componentDidUpdate(previousProps) { 54 | if ((previousProps.style !== this.props.style) || 55 | (previousProps.animatedStyle !== this.props.animatedStyle) || 56 | (previousProps.animateInitial !== this.props.animateInitial) 57 | ){ 58 | this.setState({ defaultStyle: this.getDefaultStyle() }) 59 | } 60 | 61 | if ((previousProps.style !== this.props.style) || (previousProps.animatedStyle !== this.props.animatedStyle) ){ 62 | this.setState({ animatedStyle: this.getAnimatedStyle() }) 63 | } 64 | 65 | if (this.props.active != this.state.active) { 66 | this.setState({ 67 | active: this.props.active 68 | }, () => { 69 | this.animate(); 70 | }) 71 | } 72 | 73 | } 74 | 75 | getDefaultStyle() { 76 | const { style, active, animatedStyle, animateInitial } = this.props; 77 | 78 | let styles = [style]; 79 | 80 | if(active && !animateInitial) styles.push(animatedStyle); 81 | 82 | return Object.assign({}, flattenStyle(styles)); 83 | } 84 | 85 | getAnimatedStyle() { 86 | const { style, animatedStyle } = this.props; 87 | 88 | const start = flattenStyle(style), 89 | finish = flattenStyle(animatedStyle); 90 | 91 | let styles = {}; 92 | 93 | Object.keys(finish).forEach(key => { 94 | styles[key] = this.state.animatedValue.interpolate({ 95 | inputRange: [0, 1], 96 | outputRange: this.getOutputRange(key, start, finish) 97 | }) 98 | }); 99 | 100 | return styles; 101 | } 102 | 103 | getOutputRange(key, start, finish) { 104 | start = start.hasOwnProperty(key) ? start[key] : getDefaultStyleValue(key, start); 105 | finish = finish[key]; 106 | 107 | return [ 108 | this.formatValue(key, start), 109 | this.formatValue(key, finish) 110 | ] 111 | } 112 | 113 | formatValue(key, value) { 114 | if(key.toLowerCase().indexOf('color') < 0) return value; 115 | const color = tinycolor(value); 116 | return color.toRgbString(); 117 | } 118 | 119 | animate() { 120 | const { active, useNativeDriver, duration } = this.props; 121 | 122 | this.state.animatedValue.stopAnimation(() => { 123 | Animated.timing(this.state.animatedValue, { 124 | toValue: active ? 1 : 0, 125 | useNativeDriver, 126 | duration 127 | }).start(); 128 | }); 129 | } 130 | 131 | render() { 132 | const { style, ref, children, ...props } = this.props; 133 | const { defaultStyle, animatedStyle } = this.state; 134 | 135 | return ( 136 | 140 | {children} 141 | 142 | ); 143 | } 144 | } 145 | 146 | return AnimatedStylesComponent; 147 | } 148 | 149 | export default { 150 | View: createComponent(Animated.View), 151 | Text: createComponent(Animated.Text), 152 | Image: createComponent(Animated.Image) 153 | } 154 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export const flattenStyle = (style) => { 4 | const flatStyle = Object.assign({}, StyleSheet.flatten(style)); 5 | if (flatStyle.transform) { 6 | flatStyle.transform.forEach(transform => { 7 | const key = Object.keys(transform)[0]; 8 | flatStyle[key] = transform[key]; 9 | }); 10 | delete flatStyle.transform; 11 | } 12 | return flatStyle; 13 | } 14 | 15 | const DIRECTIONAL_FALLBACKS = { 16 | Top: ['Vertical', ''], 17 | Bottom: ['Vertical', ''], 18 | Vertical: [''], 19 | Left: ['Horizontal', ''], 20 | Right: ['Horizontal', ''], 21 | Horizontal: [''], 22 | }; 23 | 24 | const DIRECTIONAL_SUFFICES = Object.keys(DIRECTIONAL_FALLBACKS); 25 | 26 | export const getDefaultStyleValue = (key, flatStyle) => { 27 | if (key === 'backgroundColor') { 28 | return 'rgba(0,0,0,0)'; 29 | } 30 | if (key === 'color' || key.indexOf('Color') !== -1) { 31 | return 'rgba(0,0,0,1)'; 32 | } 33 | if (key.indexOf('rotate') === 0 || key.indexOf('skew') === 0) { 34 | return '0deg'; 35 | } 36 | if (key === 'opacity' || key.indexOf('scale') === 0) { 37 | return 1; 38 | } 39 | if (key === 'fontSize') { 40 | return 14; 41 | } 42 | if (key.indexOf('margin') === 0 || key.indexOf('padding') === 0) { 43 | for (let suffix, i = 0; i < DIRECTIONAL_SUFFICES.length; i++) { 44 | suffix = DIRECTIONAL_SUFFICES[i]; 45 | if (key.substr(-suffix.length) === suffix) { 46 | const prefix = key.substr(0, key.length - suffix.length); 47 | const fallbacks = DIRECTIONAL_FALLBACKS[suffix]; 48 | for (let fallback, j = 0; j < fallbacks.length; j++) { 49 | fallback = prefix + fallbacks[j]; 50 | if (fallback in flatStyle) { 51 | return flatStyle[fallback]; 52 | } 53 | } 54 | break; 55 | } 56 | } 57 | } 58 | return 0; 59 | } 60 | 61 | // These styles need to be nested in a transform array 62 | const TRANSFORM_STYLE_PROPERTIES = [ 63 | 'perspective', 64 | 'rotate', 65 | 'rotateX', 66 | 'rotateY', 67 | 'rotateZ', 68 | 'scale', 69 | 'scaleX', 70 | 'scaleY', 71 | 'skewX', 72 | 'skewY', 73 | 'translateX', 74 | 'translateY', 75 | ]; 76 | 77 | // Transforms { translateX: 1 } to { transform: [{ translateX: 1 }]} 78 | export const wrapTransforms = (style) => { 79 | let wrapped = {}; 80 | Object.keys(style).forEach(key => { 81 | if (TRANSFORM_STYLE_PROPERTIES.indexOf(key) !== -1) { 82 | if (!wrapped.transform) { 83 | wrapped.transform = []; 84 | } 85 | wrapped.transform.push({ 86 | [key]: style[key], 87 | }); 88 | } else { 89 | wrapped[key] = style[key]; 90 | } 91 | }); 92 | return wrapped; 93 | } --------------------------------------------------------------------------------