├── .babelrc ├── .gitignore ├── README.md ├── components ├── assets │ └── google.jpg ├── colors.js ├── index.js └── option.js ├── demo.gif ├── exp.json ├── main.js └── package.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["babel-preset-exponent"], 3 | "env": { 4 | "development": { 5 | "plugins": ["transform-react-jsx-source"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .exponent/* 3 | npm-debug.* 4 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #react-native-fan-button 2 | Pure javascript fan button for React Native framework. The menu with options expand when clicked on. 3 | 4 | Android version is broken due to this issue: https://github.com/facebook/react-native/issues/3282 5 | 6 | If anyone knows a work-around, issue a flag. If anyone wants to resolve this and submit a PR, I'd appreciate it! :-) 7 | 8 | ##Inspiration 9 | 10 | I was inspired by [Yousef Kama's feedback UI challenge](https://medium.com/@yousefkama/react-native-ui-challenge-1-42db390905c#.vquzar3pa), so I decided to create something 11 | related to feedback UI from scratch! 12 | 13 | ###Demo 14 | ![Demo](https://raw.githubusercontent.com/ggomaeng/react-native-fan-button/master/demo.gif) 15 | 16 | Disclaimer: Google colors and image was only used to help demonstration purposes. 17 | ## Try it out 18 | 19 | Try it with Exponent: https://getexponent.com/@sungwoopark95/react-native-fan-button 20 | 21 | ## Run it locally 22 | 23 | To install, there are two steps: 24 | 25 | 1. Install Exponent XDE [following this 26 | guide](https://docs.getexponent.com/versions/latest/introduction/installation.html). 27 | Also install the Exponent app on your phone if you want to test it on 28 | your device, otherwise you don't need to do anything for the simulator. 29 | 2. Clone this repo and run `npm install` 30 | ```bash 31 | git clone https://github.com/ggomaeng/react-native-fan-button.git fan 32 | 33 | cd fan 34 | npm install 35 | ``` 36 | 3. Open the project with Exponent XDE and run it. 37 | 38 | The MIT License (MIT) 39 | ===================== 40 | 41 | Copyright © 2016 Sung Woo Park 42 | 43 | Permission is hereby granted, free of charge, to any person 44 | obtaining a copy of this software and associated documentation 45 | files (the “Software”), to deal in the Software without 46 | restriction, including without limitation the rights to use, 47 | copy, modify, merge, publish, distribute, sublicense, and/or sell 48 | copies of the Software, and to permit persons to whom the 49 | Software is furnished to do so, subject to the following 50 | conditions: 51 | 52 | The above copyright notice and this permission notice shall be 53 | included in all copies or substantial portions of the Software. 54 | 55 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 56 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 57 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 58 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 59 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 60 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 61 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 62 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /components/assets/google.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggomaeng/react-native-fan-button/29974b99c5e14d3e895183876b1db6d9da4b8384/components/assets/google.jpg -------------------------------------------------------------------------------- /components/colors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by ggoma on 12/27/16. 3 | */ 4 | export default { 5 | blue: '#4285F4', 6 | green: '#34A853', 7 | yellow: '#FBBC05', 8 | red: '#EA4335' 9 | } -------------------------------------------------------------------------------- /components/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by ggoma on 12/27/16. 3 | */ 4 | import React, {Component} from 'react'; 5 | import { 6 | Animated, 7 | PanResponder, 8 | View, 9 | Image, 10 | Text, 11 | TouchableWithoutFeedback, 12 | StyleSheet 13 | } from 'react-native'; 14 | 15 | const BUTTON_SIZE = 100; 16 | 17 | import colors from './colors'; 18 | import Option from './option'; 19 | import {Ionicons} from '@exponent/vector-icons'; 20 | 21 | export default class FanButton extends Component { 22 | state = { 23 | scale: new Animated.Value(1), 24 | pan: new Animated.ValueXY(0), 25 | opacity: new Animated.Value(1), 26 | bgColor: 'black', 27 | icon: 'md-add', 28 | options: [ 29 | {id: 0, color: colors.blue, icon: 'md-home'}, 30 | {id: 1, color: colors.green, icon: 'md-globe'}, 31 | {id: 2, color: colors.yellow, icon: 'md-map'}, 32 | {id: 3, color: colors.red, icon: 'md-happy'} 33 | ], 34 | }; 35 | 36 | 37 | px = 0; 38 | py = 0; 39 | 40 | componentWillMount() { 41 | let panMover = Animated.event([ 42 | null, {dx: this.state.pan.x, dy: this.state.pan.y}, 43 | ]); 44 | 45 | this._panResponder = PanResponder.create({ 46 | onStartShouldSetPanResponderCapture: () => true, 47 | onStartShouldSetPanResponder: () => true, 48 | onMoveShouldSetResponderCapture: () => true, 49 | onMoveShouldSetPanResponderCapture: () => true, 50 | onPanResponderMove: (e,g) => { 51 | const event = e.nativeEvent; 52 | this._hoverOn(event.pageX, event.pageY); 53 | // console.log(event.pageX, event.pageY); 54 | return panMover(e, g); 55 | }, 56 | onPanResponderGrant: (e, gestureState) => { 57 | this.state.pan.setValue({x: 0, y: 0}); 58 | console.log('pressing!'); 59 | this._showOptions(); 60 | this._onPressIn(); 61 | }, 62 | onPanResponderRelease: (e, g) => { 63 | const event = e.nativeEvent; 64 | this._releasedOn(event.pageX, event.pageY); 65 | this._hideOptions(); 66 | this._onPressOut(); 67 | } 68 | }) 69 | } 70 | 71 | componentDidMount() { 72 | setTimeout(() => { 73 | this.refs.view.measure((a, b, w, h, px, py) => { 74 | this.px = px; this.py = py; 75 | console.log(this.px, this.py); 76 | }) 77 | }, 0) 78 | } 79 | 80 | _onPressIn() { 81 | Animated.timing( 82 | this.state.scale, 83 | {toValue: 1.3} 84 | ).start(); 85 | Animated.timing( 86 | this.state.opacity, 87 | {toValue: 0, duration: 500} 88 | ).start(); 89 | } 90 | 91 | _onPressOut() { 92 | Animated.timing( 93 | this.state.scale, 94 | {toValue: 1} 95 | ).start(); 96 | Animated.timing( 97 | this.state.opacity, 98 | {toValue: 1, duration: 500} 99 | ).start(); 100 | } 101 | 102 | _hoverOn(x, y) { 103 | const {options} = this.state; 104 | options.map((option) => { 105 | this.refs[option.id].hoverOn(x, y); 106 | }) 107 | } 108 | 109 | 110 | 111 | _releasedOn(x, y) { 112 | const {options} = this.state; 113 | options.map((option) => { 114 | this.refs[option.id].releasedOn(x, y); 115 | }) 116 | } 117 | 118 | _updateIcon(color, icon) { 119 | this.setState({bgColor: color, icon}); 120 | } 121 | 122 | _updateIndex(index) { 123 | this.setState({selected: index}); 124 | this.props.updateIndex(index); 125 | } 126 | 127 | _showOptions() { 128 | const {options} = this.state; 129 | options.map((option) => { 130 | this.refs[option.id].moveOut(); 131 | this.refs[option.id].logRange(); 132 | }) 133 | } 134 | 135 | _hideOptions() { 136 | const {options} = this.state; 137 | options.map((option) => { 138 | this.refs[option.id].moveIn(); 139 | 140 | }) 141 | } 142 | 143 | _renderOptions() { 144 | const {options} = this.state; 145 | return options.map((option, i) => { 146 | return