├── .watchmanconfig ├── .gitattributes ├── .babelrc ├── index.js ├── animation.gif ├── .buckconfig ├── .gitignore ├── .flowconfig ├── __tests__ ├── CircleTransition.js └── __snapshots__ │ └── CircleTransition.js.snap ├── package.json ├── CircleTransition.js └── README.md /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-native"] 3 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import CircleTransition from './CircleTransition' 2 | export default CircleTransition 3 | -------------------------------------------------------------------------------- /animation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexbrillant/react-native-expanding-circle-transition/HEAD/animation.gif -------------------------------------------------------------------------------- /.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | # OSX 3 | # 4 | .DS_Store 5 | 6 | # Xcode 7 | # 8 | build/ 9 | *.pbxuser 10 | !default.pbxuser 11 | *.mode1v3 12 | !default.mode1v3 13 | *.mode2v3 14 | !default.mode2v3 15 | *.perspectivev3 16 | !default.perspectivev3 17 | xcuserdata 18 | *.xccheckout 19 | *.moved-aside 20 | DerivedData 21 | *.hmap 22 | *.ipa 23 | *.xcuserstate 24 | project.xcworkspace 25 | 26 | # Android/IntelliJ 27 | # 28 | build/ 29 | .idea 30 | .gradle 31 | local.properties 32 | *.iml 33 | 34 | # node.js 35 | # 36 | node_modules/ 37 | npm-debug.log 38 | yarn-error.log 39 | 40 | # BUCK 41 | buck-out/ 42 | \.buckd/ 43 | android/app/libs 44 | *.keystore 45 | 46 | # fastlane 47 | # 48 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 49 | # screenshots whenever they are needed. 50 | # For more information about the recommended setup visit: 51 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 52 | 53 | fastlane/report.xml 54 | fastlane/Preview.html 55 | fastlane/screenshots 56 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | ; We fork some components by platform 3 | .*/*[.]android.js 4 | 5 | ; Ignore "BUCK" generated dirs 6 | /\.buckd/ 7 | 8 | ; Ignore unexpected extra "@providesModule" 9 | .*/node_modules/.*/node_modules/fbjs/.* 10 | 11 | ; Ignore duplicate module providers 12 | ; For RN Apps installed via npm, "Libraries" folder is inside 13 | ; "node_modules/react-native" but in the source repo it is in the root 14 | .*/Libraries/react-native/React.js 15 | .*/Libraries/react-native/ReactNative.js 16 | 17 | [include] 18 | 19 | [libs] 20 | node_modules/react-native/Libraries/react-native/react-native-interface.js 21 | node_modules/react-native/flow 22 | flow/ 23 | 24 | [options] 25 | module.system=haste 26 | 27 | experimental.strict_type_args=true 28 | 29 | munge_underscores=true 30 | 31 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub' 32 | 33 | suppress_type=$FlowIssue 34 | suppress_type=$FlowFixMe 35 | suppress_type=$FixMe 36 | 37 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(3[0-7]\\|[1-2][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) 38 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(3[0-7]\\|1[0-9]\\|[1-2][0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+ 39 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy 40 | 41 | unsafe.enable_getters_and_setters=true 42 | 43 | [version] 44 | ^0.37.0 45 | -------------------------------------------------------------------------------- /__tests__/CircleTransition.js: -------------------------------------------------------------------------------- 1 | import 'react-native' 2 | import React from 'react' 3 | import CircleTransition from '../CircleTransition.js' 4 | 5 | // Note: test renderer must be required after react-native. 6 | import renderer from 'react-test-renderer' 7 | 8 | const positionTest = (position) => { 9 | const tree = renderer.create( 10 | 17 | ).toJSON() 18 | expect(tree).toMatchSnapshot() 19 | } 20 | 21 | it('renders correctly with default props', () => { 22 | const tree = renderer.create( 23 | 24 | ) 25 | }) 26 | 27 | it('renders correctly when position is center', () => { 28 | positionTest('center') 29 | }) 30 | 31 | it('renders correctly when position is topRight', () => { 32 | positionTest('topRight') 33 | }) 34 | 35 | it('renders correctly when position is topLeft', () => { 36 | positionTest('topLeft') 37 | }) 38 | 39 | it('renders correctly when position is bottomLeft', () => { 40 | positionTest('bottomLeft') 41 | }) 42 | 43 | it('renders correctly when position is bottomRight', () => { 44 | positionTest('bottomRight') 45 | }) 46 | 47 | it('renders correctly when position is left', () => { 48 | positionTest('left') 49 | }) 50 | 51 | it('renders correctly when position is right', () => { 52 | positionTest('right') 53 | }) 54 | 55 | it('renders correctly when position is top', () => { 56 | positionTest('top') 57 | }) 58 | 59 | it('renders correctly when position is bottom', () => { 60 | positionTest('bottom') 61 | }) 62 | 63 | it('renders correctly when position is custom', () => { 64 | positionTest('custom') 65 | }) 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-expanding-circle-transition", 3 | "version": "1.2.4", 4 | "private": false, 5 | "main": "index.js", 6 | "description": "Expanding circle transition using react-native Modal and Animated API", 7 | "keywords": [ 8 | "react-native", 9 | "react-component", 10 | "react-native-component", 11 | "react", 12 | "mobile", 13 | "ios", 14 | "ui", 15 | "animation", 16 | "transition", 17 | "circle", 18 | "expanding circle" 19 | ], 20 | "author": { 21 | "name": "Alexandre Brillant", 22 | "email": "abrillant23@gmail.com" 23 | }, 24 | "license": "MIT", 25 | "homepage": "https://github.com/alexbrillant/react-native-expanding-circle-transition", 26 | "bugs": { 27 | "url": "https://github.com/alexbrillant/react-native-expanding-circle-transition/issues" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "git://github.com/alexbrillant/react-native-expanding-circle-transition" 32 | }, 33 | "scripts": { 34 | "test": "jest --coverage", 35 | "lint": "standard --verbose | snazzy", 36 | "lintdiff": "git diff --name-only --cached --relative | grep '\\.js$' | xargs standard | snazzy", 37 | "fixcode": "standard --fix" 38 | }, 39 | "devDependencies": { 40 | "babel-eslint": "^7.1.1", 41 | "jest-cli": "^18.1.0", 42 | "react": "15.4.2", 43 | "react-native": "0.41.2", 44 | "babel-jest": "18.0.0", 45 | "babel-preset-react-native": "1.9.1", 46 | "jest": "18.1.0", 47 | "react-test-renderer": "15.4.2" 48 | }, 49 | "jest": { 50 | "preset": "react-native" 51 | }, 52 | "config": { 53 | "ghooks": { 54 | "pre-commit": "if [ -d 'ignite-base' ]; then cd ignite-base; fi; npm run lint" 55 | } 56 | }, 57 | "dependencies": { 58 | "react-mixin": "^2.0.2" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /CircleTransition.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Easing, Modal, Dimensions, Animated } from 'react-native' 4 | const { width, height } = Dimensions.get('window') 5 | const reactMixin = require('react-mixin') 6 | import TimerMixin from 'react-timer-mixin' 7 | 8 | class CircleTransition extends Component { 9 | constructor (props) { 10 | super(props) 11 | const { 12 | scaleShrink, 13 | scaleExpand, 14 | expand 15 | } = this.props 16 | const initialScale = expand ? scaleShrink : scaleExpand 17 | this.state = { 18 | visible: false, 19 | scale: new Animated.Value(initialScale) 20 | } 21 | this.setVisible = this.setVisible.bind(this) 22 | this.setScale = this.setScale.bind(this) 23 | this.resetCircle = this.resetCircle.bind(this) 24 | } 25 | 26 | start (callback) { 27 | const {expand} = this.props 28 | this.setVisible(true, () => this.animate(expand, callback)) 29 | } 30 | 31 | animate (expand, callback) { 32 | const { 33 | scaleShrink, 34 | scaleExpand, 35 | easing 36 | } = this.props 37 | let toValue = expand ? scaleExpand : scaleShrink 38 | Animated.timing(this.state.scale, { 39 | toValue: toValue, 40 | duration: this.props.duration, 41 | easing: easing 42 | }).start(() => { 43 | callback() 44 | this.hideCircle() 45 | }) 46 | } 47 | 48 | hideCircle () { 49 | const { transitionBuffer } = this.props 50 | // the circle disappears only when the transition is completed... 51 | this.setTimeout(() => { 52 | this.resetCircle() 53 | }, transitionBuffer) 54 | } 55 | 56 | resetCircle () { 57 | this.setVisible(false, () => { 58 | this.setScale(0) 59 | }) 60 | } 61 | 62 | setVisible (visible, callback) { 63 | this.setState({ 64 | visible: visible 65 | }, callback) 66 | } 67 | 68 | setScale (scale) { 69 | this.setState({ 70 | scale: new Animated.Value(scale) 71 | }) 72 | } 73 | 74 | getLeftPosition (position) { 75 | const {size, customLeftMargin} = this.props 76 | const halfSize = size / 2 77 | const halfWidth = width / 2 78 | let marginHorizontalTopLeft = -halfSize 79 | switch (position) { 80 | case 'center': 81 | case 'top': 82 | case 'bottom': 83 | return marginHorizontalTopLeft + halfWidth 84 | case 'topRight': 85 | case 'bottomRight': 86 | case 'right': 87 | return marginHorizontalTopLeft + width 88 | case 'custom': 89 | return marginHorizontalTopLeft + customLeftMargin 90 | default: 91 | return marginHorizontalTopLeft 92 | } 93 | } 94 | 95 | getTopPosition (position) { 96 | const {size, customTopMargin} = this.props 97 | const halfSize = size / 2 98 | const halfHeight = height / 2 99 | let marginVerticalTopLeft = -halfSize 100 | switch (position) { 101 | case 'center': 102 | case 'left': 103 | case 'right': 104 | return marginVerticalTopLeft + halfHeight 105 | case 'bottomLeft': 106 | case 'bottomRight': 107 | case 'bottom': 108 | return marginVerticalTopLeft + height 109 | case 'custom': 110 | return marginVerticalTopLeft + customTopMargin 111 | default: 112 | return marginVerticalTopLeft 113 | } 114 | } 115 | 116 | render () { 117 | const {scale, visible} = this.state 118 | const { size, color, position } = this.props 119 | let topPosition = this.getTopPosition(position) 120 | let leftPosition = this.getLeftPosition(position) 121 | return ( 122 | {}}> 127 | 139 | 140 | ) 141 | } 142 | } 143 | 144 | reactMixin(CircleTransition.prototype, TimerMixin) 145 | 146 | CircleTransition.propTypes = { 147 | color: PropTypes.string, 148 | size: PropTypes.number, 149 | scaleShrink: PropTypes.number, 150 | scaleExpand: PropTypes.number, 151 | duration: PropTypes.number, 152 | transitionBuffer: PropTypes.number, 153 | position: PropTypes.oneOf([ 154 | 'topLeft', 155 | 'topRight', 156 | 'bottomLeft', 157 | 'bottomRight', 158 | 'center', 159 | 'left', 160 | 'right', 161 | 'top', 162 | 'bottom', 163 | 'custom' 164 | ]), 165 | customLeftMargin: PropTypes.number, 166 | customTopMargin: PropTypes.number, 167 | expand: PropTypes.bool, 168 | easing: PropTypes.func, 169 | sizeBeforeExpanding: PropTypes.number 170 | } 171 | 172 | CircleTransition.defaultProps = { 173 | color: 'orange', 174 | size: Math.min(width, height) - 1, 175 | scaleShrink: 0, 176 | scaleExpand: 4, 177 | duration: 800, 178 | transitionBuffer: 5, 179 | position: 'topLeft', 180 | expand: true, 181 | customLeftMargin: 0, 182 | customTopMargin: 0, 183 | easing: Easing.linear 184 | } 185 | 186 | export default CircleTransition 187 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/CircleTransition.js.snap: -------------------------------------------------------------------------------- 1 | exports[`test renders correctly when position is bottom 1`] = ` 2 | 8 | 25 | 26 | `; 27 | 28 | exports[`test renders correctly when position is bottomLeft 1`] = ` 29 | 35 | 52 | 53 | `; 54 | 55 | exports[`test renders correctly when position is bottomRight 1`] = ` 56 | 62 | 79 | 80 | `; 81 | 82 | exports[`test renders correctly when position is center 1`] = ` 83 | 89 | 106 | 107 | `; 108 | 109 | exports[`test renders correctly when position is custom 1`] = ` 110 | 116 | 133 | 134 | `; 135 | 136 | exports[`test renders correctly when position is left 1`] = ` 137 | 143 | 160 | 161 | `; 162 | 163 | exports[`test renders correctly when position is right 1`] = ` 164 | 170 | 187 | 188 | `; 189 | 190 | exports[`test renders correctly when position is top 1`] = ` 191 | 197 | 214 | 215 | `; 216 | 217 | exports[`test renders correctly when position is topLeft 1`] = ` 218 | 224 | 241 | 242 | `; 243 | 244 | exports[`test renders correctly when position is topRight 1`] = ` 245 | 251 | 268 | 269 | `; 270 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-expanding-circle-transition 2 | [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/dwyl/esta/issues) 3 | [![Standard - JavaScript Style Guide](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) 4 | [![npm version](https://badge.fury.io/js/react-native-expanding-circle-transition.svg)](https://badge.fury.io/js/react-native-expanding-circle-transition) 5 | [![npm](https://img.shields.io/badge/downloads-627%2Fmonth-green.svg)](https://www.npmjs.com/package/react-native-expanding-circle-transition) 6 | 7 | ## Preview 8 | 9 | ![App preview](/animation.gif) 10 | 11 | ## Installation 12 | 13 | `npm install react-native-expanding-circle-transition --save` 14 | 15 | ## Props 16 | 17 | | Props | type | description | required or default | 18 | |----------|--------|---------------------------------------------------------------------------------------------------------|----------------------------------| 19 | | color | string | Color of the circle view | 'orange' | 20 | | size | number | Size of the circle view. Important: It has to fit in the window | Math.min(width, height) - 1 | 21 | | scaleShrink | number | Scale factor to shrink the circle | 0 | 22 | | scaleExpand | number | Scale factor to expand the circle | 4 | 23 | | transitionBuffer | number | Buffer between the transition and the animation. The transition must happen before the circle is hidden | 5 | 24 | | duration | number | Animation duration | 800 | 25 | | expand | bool | Expand the circle if true, reduce the circle if false | true | 26 | | position | enum | Circle position : ['topLeft', 'topRight', 'bottomLeft', 'bottomRight', 'center', 'left', 'right', 'top', 'bottom', 'custom'] | 'topLeft' | 27 | | customLeftMargin | number | Custom position's left margin from the center of the circle positioned at topLeft | 0 | 28 | | customTopMargin | number | Custom position's top margin from the center of the circle positioned at topLeft | 0 | 29 | | easing | func | React Native Animation Easing | Easing.linear | 30 | 31 | ## How to use 32 | 33 | To trigger the animation, you need to use a ref to call the start function of this component. 34 | To change the scene before the circle is hidden, pass a callback(check out usage exemple handlePress function). 35 | 36 | ## Usage exemples 37 | ```javascript 38 | import React, { 39 | Component 40 | } from 'react' 41 | 42 | import { 43 | Easing, 44 | StyleSheet, 45 | Text, 46 | View, 47 | TouchableWithoutFeedback 48 | } from 'react-native' 49 | 50 | import CircleTransition from 'react-native-expanding-circle-transition' 51 | const ANIMATION_DURATION = 1200 52 | const INITIAL_VIEW_BACKGROUND_COLOR = '#E3E4E5' 53 | const CIRCLE_COLOR1 = '#29C5DB' 54 | const CIRCLE_COLOR2 = '#4EB8AE' 55 | const CIRCLE_COLOR3 = '#81C781' 56 | const CIRCLE_COLOR4 = '#B0D882' 57 | const TRANSITION_BUFFER = 10 58 | const POSITON = 'custom' 59 | 60 | const reactMixin = require('react-mixin') 61 | import TimerMixin from 'react-timer-mixin' 62 | 63 | class Exemples extends Component { 64 | constructor (props) { 65 | super(props) 66 | this.state = { 67 | viewBackgroundColor: INITIAL_VIEW_BACKGROUND_COLOR, 68 | circleColor: CIRCLE_COLOR1, 69 | customLeftMargin: 0, 70 | customTopMargin: 0, 71 | counter: 0 72 | } 73 | this.handlePress = this.handlePress.bind(this) 74 | this.changeColor = this.changeColor.bind(this) 75 | } 76 | 77 | handlePress (event) { 78 | let pressLocationX = event.nativeEvent.locationX 79 | let pressLocationY = event.nativeEvent.locationY 80 | this.setState({ 81 | customLeftMargin: pressLocationX, 82 | customTopMargin: pressLocationY 83 | }, this.circleTransition.start(this.changeColor)) 84 | } 85 | 86 | changeColor () { 87 | const { circleColor, counter } = this.state 88 | let newCounter = counter < 3 ? counter + 1 : 0 89 | let newCircleColor = this.getColor(newCounter) 90 | this.setState({ 91 | viewBackgroundColor: circleColor, 92 | counter: newCounter 93 | }) 94 | this.changeCircleColor(newCircleColor) 95 | } 96 | 97 | changeCircleColor (newCircleColor) { 98 | this.setTimeout(() => { 99 | this.setState({ 100 | circleColor: newCircleColor 101 | }) 102 | }, TRANSITION_BUFFER + 5) 103 | } 104 | 105 | getColor (counter) { 106 | switch (counter) { 107 | case 0: 108 | return CIRCLE_COLOR1 109 | case 1: 110 | return CIRCLE_COLOR2 111 | case 2: 112 | return CIRCLE_COLOR3 113 | case 3: 114 | return CIRCLE_COLOR4 115 | default: 116 | return CIRCLE_COLOR4 117 | } 118 | } 119 | 120 | render () { 121 | let { 122 | circleColor, 123 | viewBackgroundColor, 124 | customTopMargin, 125 | customLeftMargin 126 | } = this.state 127 | return ( 128 | 133 | 136 | 137 | {viewBackgroundColor.toString()} 138 | 139 | 140 | { this.circleTransition = circle }} 142 | color={circleColor} 143 | expand 144 | customTopMargin={customTopMargin} 145 | customLeftMargin={customLeftMargin} 146 | transitionBuffer={TRANSITION_BUFFER} 147 | duration={ANIMATION_DURATION} 148 | easing={Easing.linear} 149 | position={POSITON} 150 | /> 151 | 152 | ) 153 | } 154 | } 155 | 156 | reactMixin(Exemples.prototype, TimerMixin) 157 | 158 | const styles = StyleSheet.create({ 159 | container: { 160 | flex: 1, 161 | flexDirection: 'column', 162 | justifyContent: 'center', 163 | alignItems: 'stretch' 164 | }, 165 | touchableView: { 166 | flex: 1, 167 | alignItems: 'center', 168 | justifyContent: 'center' 169 | }, 170 | text: { 171 | fontSize: 45, 172 | fontWeight: '400', 173 | color: '#253039' 174 | }, 175 | touchable: { 176 | flex: 1, 177 | flexDirection: 'row', 178 | alignItems: 'center', 179 | justifyContent: 'center' 180 | } 181 | }) 182 | 183 | export default Exemples 184 | --------------------------------------------------------------------------------