├── .gitignore ├── package.json ├── LICENSE ├── README.md └── JellySideMenu.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | node_modules 3 | AwesomeProject.xcodeproj 4 | AwesomeProjectTests 5 | index.ios.js 6 | iOS 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-jelly-side-menu", 3 | "version": "0.0.9", 4 | "description": "A side menu that animates like a jelly! iOS & Android tested.", 5 | "main": "JellySideMenu.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/VansonLeung/react-native-jelly-side-menu.git" 12 | }, 13 | "tags": [ 14 | "react", 15 | "react-native", 16 | "react-component", 17 | "ios", 18 | "android", 19 | "menu", 20 | "drawer" 21 | ], 22 | "keywords": [ 23 | "react", 24 | "react-native", 25 | "ios", 26 | "android", 27 | "react-component", 28 | "menu", 29 | "drawer" 30 | ], 31 | "author": "Vanson Wing Leung ", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/VansonLeung/react-native-jelly-side-menu/issues" 35 | }, 36 | "homepage": "https://github.com/VansonLeung/react-native-jelly-side-menu#readme", 37 | "dependencies": { 38 | "react-native": ">=0.25.0", 39 | "rebound": ">=0.0.13" 40 | }, 41 | "peerDependencies": { 42 | "react-native-svg": ">=2.2.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Vanport Software Consultancy Limited 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WARNING: DEPRECATED SORRY 2 | 3 | # react-native-jelly-side-menu 4 | A side menu that animates like a jelly! iOS & Android tested. 5 | 6 | [![npm version](https://badge.fury.io/js/react-native-jelly-side-menu.svg)](http://badge.fury.io/js/react-native) 7 | [![NPM](https://nodei.co/npm/react-native-jelly-side-menu.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/react-native-jelly-side-menu/) 8 | 9 | ## Demo Showcase (Youtube link) The video seems stuck at 2.5s when not using Chrome to watch. I will fix it when I have time. 10 | [https://www.youtube.com/watch?v=lvyJX2-l8pM](https://www.youtube.com/watch?v=lvyJX2-l8pM) 11 | ## 12 | [![Demo Showcase](https://img.youtube.com/vi/lvyJX2-l8pM/0.jpg)](https://www.youtube.com/watch?v=lvyJX2-l8pM) 13 | ## 14 | Demo Showcase 15 | 16 | 17 | ## Prerequisites 18 | React Native 0.25 or higher 19 | 20 | ## Installation 21 | ```shell 22 | npm i react-native-svg --save 23 | npm i react-native-jelly-side-menu --save 24 | rnpm link 25 | ``` 26 | 27 | ## Usage 28 | ```js 29 | import JellySideMenu from 'react-native-jelly-side-menu' 30 | ``` 31 | 32 | ### Toggle 33 | ```js 34 | toggleSideMenu() 35 | ``` 36 | 37 | ### Open 38 | ```js 39 | toggleSideMenu(true) 40 | ``` 41 | 42 | ### Close 43 | ```js 44 | toggleSideMenu(false) 45 | ``` 46 | 47 | 48 | ### Example 49 | ```js 50 | class JellySideMenuPage extends Component { 51 | 52 | constructor(props) { 53 | super(props); 54 | this.itemStyle = {padding: 16, backgroundColor: 'transparent'}; 55 | this.itemTextStyle = {color: '#000000', fontWeight: 'bold', fontSize: 20}; 56 | this.renderMenu = this.renderMenu.bind(this); 57 | } 58 | 59 | renderItem(text, onPress) { 60 | return ( 61 | 62 | 63 | {text} 64 | 65 | 66 | ) 67 | } 68 | 69 | renderMenu() { 70 | return ( 71 | 72 | {this.renderItem("Jelly Side Menu by Vanport", () => {})} 73 | {this.renderItem("Toggle Side Menu", () => {this.jsm.toggleSideMenu();})} 74 | {this.renderItem("Close Side Menu", () => {this.jsm.toggleSideMenu(false);})} 75 | 76 | ) 77 | } 78 | 79 | render() { 80 | return ( 81 | 82 | {this.jsm = view}} 84 | fill={"#FFF"} fillOpacity={1.0} 85 | renderMenu={this.renderMenu}> 86 | 87 | {this.jsm.toggleSideMenu(true)}}> 88 | 89 | Open Jelly Menu 90 | 91 | 92 | 93 | 94 | 95 | ) 96 | } 97 | } 98 | ``` 99 | 100 | 101 | ### Milestones 102 | ``` 103 | ✔ iOS Support @done (16-07-09 12:07) 104 | ✔ Android Support @done (16-07-09 12:07) 105 | ☐ Context control toggle support 106 | ✔ Remove unnecessary imports 107 | ☐ Add Essential control parameters support (enable/disable menu, enable/disable gesture, set default state) 108 | ☐ Add Callback support (onWillOpen(state), onDidOpen(state), onWillClose(state), onDidClose(state)) 109 | ☐ Add Menu State check function (getState() => State) 110 | ☐ Add Fine tune control parameters support (thresholds of dragging, menu width, bounce tension & friction) 111 | ☐ Add Side menu docking direction support (top, left, right, bottom) 112 | ☐ Cater device orientation change 113 | ☐ Performance improvement 114 | -------------------------------------------------------------------------------- /JellySideMenu.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import rebound from 'rebound'; 4 | 5 | import React, { Component } from 'react'; 6 | import { 7 | Platform, 8 | Animated, 9 | PanResponder, 10 | Dimensions, 11 | View, 12 | } from 'react-native'; 13 | 14 | import Svg, { Path } from 'react-native-svg'; 15 | 16 | var { width, height } = Dimensions.get('window'); 17 | 18 | class JellySideMenu extends Component { 19 | constructor(props) { 20 | super(props); 21 | this.state = { 22 | is_dock : false, 23 | offsetDragX : 0, 24 | offsetDragY : height / 2, 25 | } 26 | this.makePanResponder(); 27 | 28 | this.onJellyUndocked = this.onJellyUndocked.bind(this); 29 | this.onJellyNotUndocked = this.onJellyNotUndocked.bind(this); 30 | } 31 | 32 | makePanResponder() { 33 | var self = this; 34 | this.panResponder = PanResponder.create({ 35 | onStartShouldSetPanResponder: function(e, gestureState) { 36 | return true 37 | }, 38 | onMoveShouldSetPanResponder: function(e, gestureState) { 39 | return true 40 | }, 41 | onPanResponderGrant: function(e, gestureState) { 42 | self.onDropSideMenuSvg() 43 | }, 44 | onPanResponderMove: function(e, gestureState) { 45 | self.onDragSideMenuSvg(gestureState.dx, gestureState.moveY) 46 | }, 47 | onPanResponderRelease: function(e, gestureState) { 48 | if (gestureState.dx > 100) { 49 | self.onDropSideMenuSvg(true) 50 | } else { 51 | self.onDropSideMenuSvg(false) 52 | } 53 | }, 54 | onPanResponderTerminate: function(e, gestureState) { 55 | if (gestureState.dx > 100) { 56 | self.onDropSideMenuSvg(true) 57 | } else { 58 | self.onDropSideMenuSvg(false) 59 | } 60 | }, 61 | }) 62 | } 63 | 64 | getPanHandlers() { 65 | return this.panResponder.panHandlers; 66 | } 67 | 68 | onDragSideMenuSvg(x, y) { 69 | this.refs.sideMenuSvgWrapper ? this.refs.sideMenuSvgWrapper.onJellyNotUndocked() : {}; 70 | this.refs.sideMenuSvg ? this.refs.sideMenuSvg.setOffsetDrag(x, y, false) : {}; 71 | } 72 | 73 | onDropSideMenuSvg(bool) { 74 | if (bool) { 75 | if (!this.state.is_dock) 76 | { 77 | this.setState({ 78 | is_dock: true 79 | }) 80 | } 81 | this.refs.sideMenuSvgWrapper ? this.refs.sideMenuSvgWrapper.onJellyNotUndocked() : {}; 82 | this.refs.sideMenuSvg ? this.refs.sideMenuSvg.dockOffsetDrag(true) : {}; 83 | } else { 84 | if (this.state.is_dock) 85 | { 86 | this.setState({ 87 | is_dock: false 88 | }) 89 | } 90 | this.refs.sideMenuSvg ? this.refs.sideMenuSvg.resetOffsetDrag(true) : {}; 91 | } 92 | } 93 | 94 | toggleSideMenu(bool) { 95 | if (bool == undefined) { 96 | this.onDropSideMenuSvg(!this.state.is_dock); 97 | } else { 98 | this.onDropSideMenuSvg(bool); 99 | } 100 | } 101 | 102 | 103 | 104 | onJellyNotUndocked() { 105 | this.refs.sideMenuSvgWrapper ? this.refs.sideMenuSvgWrapper.onJellyNotUndocked() : {}; 106 | } 107 | 108 | 109 | onJellyUndocked() { 110 | this.refs.sideMenuSvgWrapper ? this.refs.sideMenuSvgWrapper.onJellyUndocked() : {}; 111 | } 112 | 113 | 114 | render() { 115 | var dockPullWidth = 20; 116 | var dockWidth = 240; 117 | 118 | var offsetDragX = this.state.offsetDragX; 119 | var offsetDragY = this.state.offsetDragY; 120 | var pathSide = " 0 0 q " + offsetDragX + " " + offsetDragY + " 0 " + height; 121 | 122 | var dockStyle = {width: dockPullWidth}; 123 | if (this.state.is_dock) {dockStyle = {width: null, right: 0}}; 124 | 125 | return ( 126 | 127 | {this.props.children} 128 | 129 | 130 | { 131 | this.renderSvg(dockWidth) 132 | } 133 | 134 | {this.props.renderMenu()} 135 | 136 | 137 | ) 138 | } 139 | 140 | 141 | renderSvg(dockWidth) { 142 | console.log("sRwdd"); 143 | if (Platform.OS === "ios") { 144 | return ( 145 | 146 | 150 | 151 | ) 152 | } 153 | 154 | return ( 155 | 156 | 157 | 161 | 162 | 163 | ) 164 | 165 | } 166 | } 167 | 168 | 169 | class JellySideMenuSvgWrapper extends Component { 170 | constructor(props) { 171 | super(props); 172 | this.is_undocked = true; 173 | this.is_mounted = true; 174 | } 175 | 176 | componentDidMount() { 177 | this.is_mounted = true; 178 | this.forceUpdate(); 179 | } 180 | 181 | componentWillUnmount() { 182 | this.is_mounted = false; 183 | } 184 | 185 | onJellyNotUndocked() { 186 | this.is_undocked = false; 187 | if (this.is_mounted) { 188 | this.forceUpdate(); 189 | } 190 | } 191 | 192 | 193 | onJellyUndocked() { 194 | this.is_undocked = true; 195 | if (this.is_mounted) { 196 | this.forceUpdate(); 197 | } 198 | } 199 | 200 | 201 | render() { 202 | console.log("sR"); 203 | if (this.is_undocked) { 204 | return null 205 | } 206 | 207 | return ( 208 | 213 | {this.props.children} 214 | 215 | ) 216 | } 217 | } 218 | 219 | 220 | class JellySideMenuContent extends Component { 221 | constructor(props) { 222 | super(props); 223 | this.state = { 224 | dockOpacityAnim : new Animated.Value(0), 225 | dockLeftAnim : new Animated.Value(-this.props.dockWidth), 226 | } 227 | } 228 | componentWillUpdate(nextProps, nextState) { 229 | if (nextProps.is_dock != this.props.is_dock) { 230 | if (nextProps.is_dock) { 231 | Animated.spring(this.state.dockOpacityAnim, {toValue: 1, friction: 10}).start() 232 | Animated.spring(this.state.dockLeftAnim, {toValue: 0, friction: 5}).start() 233 | } else { 234 | Animated.spring(this.state.dockOpacityAnim, {toValue: 0, friction: 10}).start() 235 | Animated.spring(this.state.dockLeftAnim, {toValue: -this.props.dockWidth, friction: 5}).start() 236 | } 237 | } 238 | } 239 | render() { 240 | var style = {width: this.props.dockWidth, position: 'absolute', top: 0, opacity: this.state.dockOpacityAnim, left: this.state.dockLeftAnim, bottom: 0, backgroundColor: 'transparent'} 241 | return ( 242 | 243 | {this.props.children} 244 | 245 | ) 246 | } 247 | } 248 | 249 | 250 | class JellySideMenuSvg extends Component { 251 | constructor(props) { 252 | super(props); 253 | 254 | this.state = { 255 | is_dock : false, 256 | is_undocked : true, 257 | offsetDragX : 0, 258 | offsetDragY : height / 2, 259 | offsetDragXSm : 0, 260 | } 261 | 262 | this.is_mounted = true; 263 | 264 | this.isBusy = false 265 | this.isBusyY = false 266 | this.isBusySm = false 267 | 268 | this.springSystem = new rebound.SpringSystem() 269 | this.springSystem2 = new rebound.SpringSystem() 270 | this.ssOffsetDragX = this.springSystem.createSpring() 271 | this.ssOffsetDragY = this.springSystem.createSpring() 272 | this.ssOffsetDragX.setCurrentValue(0) 273 | this.ssOffsetDragY.setCurrentValue(height / 2) 274 | this.ssOffsetDragX.addListener({onSpringUpdate: () => { 275 | if (!this.is_mounted) { 276 | return 277 | } 278 | 279 | if (this.isBusy) { 280 | return 281 | } 282 | this.isBusy = true; 283 | if (this.ssOffsetDragX.getEndValue() <= 0) { 284 | if (this.state.offsetDragX <= 0 && !this.state.is_undocked) { 285 | this.setState({offsetDragX: this.ssOffsetDragX.getCurrentValue(), is_undocked: true}); 286 | this.ssOffsetDragX.setCurrentValue(0); 287 | this.ssOffsetDragXSm.setCurrentValue(0); 288 | return; 289 | } else { 290 | this.setState({offsetDragX: this.ssOffsetDragX.getCurrentValue()}); 291 | return; 292 | } 293 | } else { 294 | if (this.state.is_undocked) { 295 | this.setState({offsetDragX: this.ssOffsetDragX.getCurrentValue(), is_undocked: false}); 296 | return; 297 | } else { 298 | this.setState({offsetDragX: this.ssOffsetDragX.getCurrentValue()}); 299 | return; 300 | } 301 | } 302 | }}) 303 | this.ssOffsetDragY.addListener({onSpringUpdate: () => { 304 | if (!this.is_mounted) { 305 | return 306 | } 307 | 308 | if (this.isBusyY) { 309 | return 310 | } 311 | this.isBusyY = true; 312 | this.setState({offsetDragY: this.ssOffsetDragY.getCurrentValue()})} 313 | }) 314 | this.ssOffsetDragXSm = this.springSystem2.createSpring() 315 | this.ssOffsetDragXSm.setCurrentValue(0) 316 | this.ssOffsetDragXSm.addListener({onSpringUpdate: () => { 317 | if (!this.is_mounted) { 318 | return 319 | } 320 | 321 | if (this.isBusySm) { 322 | return 323 | } 324 | this.isBusySm = true; 325 | this.setState({offsetDragXSm: this.ssOffsetDragXSm.getCurrentValue()})} 326 | }) 327 | 328 | var sscX = this.ssOffsetDragX.getSpringConfig(); 329 | var sscY = this.ssOffsetDragY.getSpringConfig(); 330 | var sscXSm = this.ssOffsetDragXSm.getSpringConfig(); 331 | 332 | sscX.tension = 500; 333 | sscX.friction = 10; 334 | 335 | sscY.tension = 500; 336 | sscY.friction = 10; 337 | 338 | sscXSm.tension = 500; 339 | sscXSm.friction = 15; 340 | } 341 | 342 | componentDidMount() { 343 | this.is_mounted = true; 344 | this.isBusy = false; 345 | this.isBusyY = false; 346 | this.isBusySm = false; 347 | } 348 | 349 | componentWillUnmount() { 350 | this.is_mounted = false; 351 | } 352 | 353 | 354 | componentWillUpdate(nextProps, nextState) { 355 | if (nextState.is_undocked != this.state.is_undocked) { 356 | if (nextState.is_undocked == true) { 357 | this.props.onJellyUndocked(); 358 | } else { 359 | this.props.onJellyNotUndocked(); 360 | } 361 | } 362 | } 363 | 364 | 365 | setOffsetDrag(x, y, animated) { 366 | if (animated) { 367 | this.ssOffsetDragX.setEndValue(x / 2); 368 | this.ssOffsetDragY.setEndValue(y); 369 | this.ssOffsetDragXSm.setEndValue(x / 5); 370 | } else { 371 | this.ssOffsetDragX.setCurrentValue(x / 2); 372 | this.ssOffsetDragY.setCurrentValue(y); 373 | this.ssOffsetDragXSm.setCurrentValue(x / 5); 374 | } 375 | } 376 | 377 | resetOffsetDrag(animated) { 378 | if (animated) { 379 | this.ssOffsetDragX.setEndValue(0); 380 | this.ssOffsetDragY.setEndValue(height / 2); 381 | this.ssOffsetDragXSm.setEndValue(0); 382 | } else { 383 | this.ssOffsetDragX.setCurrentValue(0); 384 | this.ssOffsetDragY.setCurrentValue(height / 2); 385 | this.ssOffsetDragXSm.setCurrentValue(0); 386 | } 387 | } 388 | 389 | dockOffsetDrag(animated) { 390 | if (animated) { 391 | this.ssOffsetDragX.setEndValue(this.props.dockWidth); 392 | this.ssOffsetDragY.setEndValue(height / 2); 393 | this.ssOffsetDragXSm.setEndValue(this.props.dockWidth); 394 | } else { 395 | this.ssOffsetDragX.setCurrentValue(this.props.dockWidth); 396 | this.ssOffsetDragY.setCurrentValue(height / 2); 397 | this.ssOffsetDragXSm.setCurrentValue(this.props.dockWidth); 398 | } 399 | } 400 | 401 | render() { 402 | this.isBusy = false; 403 | this.isBusyY = false; 404 | this.isBusySm = false; 405 | 406 | var offsetDragX = this.state.offsetDragX; 407 | var offsetDragY = this.state.offsetDragY; 408 | var offsetDragXSm = this.state.offsetDragXSm; 409 | var pathSide = ""; 410 | var path = ""; 411 | 412 | pathSide = "M" + offsetDragXSm + " 0"; 413 | pathSide += " Q" + offsetDragX + " " + 0 + " " + offsetDragX + " " + offsetDragY; 414 | pathSide += " Q" + offsetDragX + " " + height + " " + offsetDragXSm + " " + height; 415 | path = pathSide + " L" + " 0 " + this.props.height + " L0 0 Z"; 416 | 417 | return ( 418 | 422 | ) 423 | } 424 | } 425 | 426 | 427 | 428 | module.exports = JellySideMenu; 429 | --------------------------------------------------------------------------------