├── .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 | 
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
148 | })
149 | }
150 |
151 | _getStyle() {
152 | const size = BUTTON_SIZE;
153 | const {scale, opacity} = this.state;
154 | return {
155 | width: size, height: size, justifyContent: 'center', alignItems: 'center', backgroundColor: 'white', borderRadius: size/2,
156 | borderWidth: 1,
157 | opacity,
158 | transform: [{scale : scale}]
159 | }
160 | }
161 |
162 | _getStyle2() {
163 | const size = BUTTON_SIZE;
164 | const {bgColor} = this.state;
165 | return {
166 | position: 'absolute', top: 0, width: size, height: size, justifyContent: 'center',
167 | alignItems: 'center', backgroundColor: bgColor, borderRadius: size/2,
168 | }
169 | }
170 |
171 | render() {
172 | const size = BUTTON_SIZE;
173 | const {icon} = this.state;
174 | return (
175 |
176 | {this._renderOptions()}
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 | )
185 | }
186 | }
187 |
--------------------------------------------------------------------------------
/components/option.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by ggoma on 12/27/16.
3 | */
4 | import React, {Component} from 'react';
5 | import {
6 | Animated,
7 | View,
8 | Text,
9 | StyleSheet
10 | } from 'react-native';
11 |
12 | import {Ionicons} from '@exponent/vector-icons';
13 |
14 | export default class Option extends Component {
15 | state = {
16 | move: new Animated.ValueXY(0),
17 | scale: new Animated.Value(1)
18 | };
19 |
20 | validRange = {
21 | x0: 0,
22 | x1: 0,
23 | y0: 0,
24 | y1: 0
25 | };
26 |
27 | componentDidMount() {
28 | const {number, size} = this.props;
29 | console.log('why is measure not working', number ,size);
30 | setTimeout(() => this.refs.view.measure((a, b, w, h, px, py) => {
31 | let offset = {x: 0, y: 0};
32 | switch (number) {
33 | case 0:
34 | offset = {x: -(size - size/4 * number), y: 0};
35 | break;
36 | case 1:
37 | offset = {x: -(size - size/2 * number), y: -(size - size/6 * number)};
38 | break;
39 | case 2:
40 | offset = {x: (size - size/4 * number), y: -(size - size/12 * number)};
41 | break;
42 | case 3:
43 | offset = {x: (size), y: 0};
44 |
45 | }
46 | this.validRange.x0 = offset.x + px;
47 | this.validRange.x1 = offset.x + w + px;
48 | this.validRange.y0 = offset.y + py;
49 | this.validRange.y1 = offset.y + h + py;
50 | console.log(this.validRange);
51 | }), 0);
52 | }
53 |
54 | hoverOn(x, y) {
55 | if(this.verify(x, y)) {
56 | console.log('hovering on:', this.props.number);
57 | this.zoomIn();
58 | return true;
59 | }
60 |
61 | this.zoomOut();
62 | return false;
63 | }
64 |
65 | releasedOn(x, y) {
66 | this.zoomOut();
67 | if(this.verify(x, y)) {
68 | console.log('released on:', this.props.number);
69 |
70 | }
71 | }
72 |
73 | zoomIn() {
74 | const {color, icon, number} = this.props;
75 | this.props.updateIcon(color, icon);
76 | this.props.updateIndex(number);
77 | Animated.spring(
78 | this.state.scale,
79 | {toValue: 1.2}
80 | ).start();
81 | }
82 |
83 | zoomOut() {
84 |
85 | Animated.spring(
86 | this.state.scale,
87 | {toValue: 1}
88 | ).start();
89 |
90 | }
91 |
92 | verify(x, y) {
93 | if((x > this.validRange.x0 && x < this.validRange.x1) && (y > this.validRange.y0 && y < this.validRange.y1)) {
94 | return true;
95 | }
96 |
97 | return false;
98 | }
99 |
100 | logRange() {
101 | console.log(this.validRange)
102 | }
103 |
104 | _getStyle() {
105 | const {size, color} = this.props;
106 | const offset = size/4;
107 | const {scale, move} = this.state;
108 | return {
109 | width: size/2, height: size/2, backgroundColor: color, borderRadius: size/4,
110 | justifyContent: 'center', alignItems: 'center',
111 | transform: [
112 | {
113 | translateX: move.x
114 | },
115 | {
116 | translateY: move.y
117 | },
118 | {
119 | scale
120 | }
121 | ]
122 | }
123 | }
124 |
125 | moveOut() {
126 | const {number, size} = this.props;
127 | let offset = {x: 0, y: 0};
128 | switch (number) {
129 | case 0:
130 | offset = {x: -(size - size/4 * number), y: 0};
131 | break;
132 | case 1:
133 | offset = {x: -(size - size/2 * number), y: -(size - size/6 * number)};
134 | break;
135 | case 2:
136 | offset = {x: (size - size/4 * number), y: -(size - size/12 * number)};
137 | break;
138 | case 3:
139 | offset = {x: (size), y: 0};
140 | }
141 |
142 | Animated.timing(
143 | this.state.move,
144 | {toValue: offset}
145 | ).start();
146 | }
147 |
148 | moveIn() {
149 | Animated.timing(
150 | this.state.move,
151 | {toValue: 0}
152 | ).start();
153 | }
154 |
155 | render() {
156 |
157 | const {size, icon} = this.props;
158 | const offset = size/4;
159 |
160 | return (
161 | {}} style={{position: 'absolute', top: offset, left: offset}}>
162 |
163 |
164 |
165 |
166 | )
167 | }
168 | }
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ggomaeng/react-native-fan-button/29974b99c5e14d3e895183876b1db6d9da4b8384/demo.gif
--------------------------------------------------------------------------------
/exp.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-fan-button",
3 | "description": "An empty new project",
4 | "slug": "react-native-fan-button",
5 | "sdkVersion": "12.0.0",
6 | "version": "1.0.0",
7 | "orientation": "portrait",
8 | "primaryColor": "#cccccc",
9 | "iconUrl": "https://s3.amazonaws.com/exp-brand-assets/ExponentEmptyManifest_192.png",
10 | "notification": {
11 | "iconUrl": "https://s3.amazonaws.com/exp-us-standard/placeholder-push-icon-blue-circle.png",
12 | "color": "#000000"
13 | },
14 | "loading": {
15 | "iconUrl": "https://s3.amazonaws.com/exp-brand-assets/ExponentEmptyManifest_192.png",
16 | "hideExponentText": false
17 | },
18 | "packagerOpts": {
19 | "assetExts": ["ttf", "mp4"]
20 | },
21 | "ios": {
22 | "supportsTablet": true
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | import Exponent from 'exponent';
2 | import React from 'react';
3 | import {
4 | StyleSheet,
5 | StatusBar,
6 | Text,
7 | View,
8 | } from 'react-native';
9 |
10 | import FanButton from './components/index';
11 | import {Ionicons} from '@exponent/vector-icons';
12 | import colors from './components/colors';
13 |
14 | class App extends React.Component {
15 | state = {
16 | index: -1
17 | };
18 | render() {
19 | const {index} = this.state;
20 |
21 | return (
22 |
23 |
24 |
25 |
26 | react-
27 | native-
28 | fan-
29 | button
30 |
31 | GitHub : ggomaeng
32 |
33 |
34 | this.setState({index})}/>
35 | Selected
36 | {index}
37 |
38 |
39 | );
40 | }
41 | }
42 |
43 | const styles = StyleSheet.create({
44 | container: {
45 | flex: 1,
46 | backgroundColor: '#f6f6f6',
47 | },
48 | title: {
49 | paddingTop: 100,
50 | alignItems: 'center',
51 | justifyContent: 'center'
52 | },
53 | content: {
54 | flex: 1,
55 | alignItems: 'center',
56 | justifyContent: 'center',
57 | }
58 | });
59 |
60 | Exponent.registerRootComponent(App);
61 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-fan-button",
3 | "version": "0.0.0",
4 | "description": "Hello Exponent!",
5 | "author": null,
6 | "private": true,
7 | "main": "main.js",
8 | "dependencies": {
9 | "exponent": "~12.0.3",
10 | "@exponent/vector-icons": "~2.0.3",
11 | "react": "~15.3.2",
12 | "react-native": "git+https://github.com/exponentjs/react-native#sdk-12.0.0"
13 | }
14 | }
--------------------------------------------------------------------------------