Hi! I've been working on this for a couple days now and just can't figure it out. I've gotten the menu to appear vertically instead of horizontally, and messed with some of the offsets & margins to make it align properly. I also eliminated content from overlay, and only kept frontView – with frontView serving as my menu, resting on top of my content that is beneath. I did it this way because I already had a lot of code for the app done, and this seemed simpler (is this my problem?).
So, here's the issue: whenever I launch the app, or navigate to a new screen, the menu appears front and center. Changing variables like touchToClose and isOpen aren't fixing it. Here are more details and some thoughts:
Is it possible that iOS (or react native) doesn’t allow an item to be rendered off screen initially? I ask because of this behavior:
- Launch app: Menu appears on screen, though off by a few pixels. This is what I want to prevent from happening.
- Toggle menu away: Menu slides off of screen, north of the screen’s bounds, just as I want. Good.
- Toggle menu back in: Menu slides back into screen, in the precise location I want it. Also good.
When I change the openMenuOffset to something extreme (too far south), it still appears in the exact same spot for (1). But, for (3), it behaves properly and falls too far south.
Another interesting thing is whenever I load a new screen, menu reappears as in (1).
Any thoughts? I'm also happy to revert back to your menu and redo my vertical implementation if you have a better idea on how to execute. I essentially want my logo in the center to launch a slide-down menu from top to bottom. I'm using react-native-router, and setting
Thank you!
INDEX.js
(Note: I changed "left" to "top", just for my own sake / preventing confusion)
var React = require('react-native');
var deviceScreen = require('Dimensions').get('window');
var styles = require('./styles');
var queueAnimation = require('./animations');
var {
PanResponder,
View,
TouchableWithoutFeedback,
Component,
} = React;
/**
* Default open menu offset. Describes a size of the amount you can
* move content view from the top and release without opening it
* @type {Number}
*/
var openMenuOffset = deviceScreen.height * 1/50;
/**
* Content view offset in the `hidden` state
* @type {Number}
*/
var hiddenMenuOffset = -655;
/**
* Size of the amount you can move content view in the opened menu state and
* release without menu closing
* @type {Number}
*/
var barrierForward = deviceScreen.height / 4;
/**
* Check if the current gesture offset bigger than allowed one
* before opening menu
* @param {Number} dx Gesture offset from the top Top of the window
* @return {Boolean}
*/
function shouldOpenMenu(dx: Number) {
return dx > barrierForward;
}
/**
* no operation function. does nothing.
*/
function noop() {}
class TopMenu extends Component {
constructor(props) {
super(props);
/**
* Current state of the menu, whether it is open or not
* @type {Boolean}
*/
this.isOpen = false;
/**
* Current style `top` attribute
* @todo Check if it's possible to avoid using `top`
* @type {Number}
*/
this.top = 0;
/**
* Default top offset for content view
* @todo Check if it's possible to avoid using `prevtop`
* @type {Number}
*/
this.prevtop = 0;
}
/**
* Creates PanResponders and links to appropriate functions
* @return {Void}
*/
createResponders(disableGestures) {
if (disableGestures || false) {
this.responder = PanResponder.create({});
return;
}
this.responder = PanResponder.create({
onMoveShouldSetPanResponder: this.handleMoveShouldSetPanResponder.bind(this),
onPanResponderMove: this.handlePanResponderMove.bind(this),
onPanResponderRelease: this.handlePanResponderEnd.bind(this),
});
}
/**
* Set the initial responders
* @return {Void}
*/
componentWillMount() {
this.createResponders(this.props.disableGestures);
}
/**
* Update responders on new props whenever possible
* @return {Void}
*/
componentWillReceiveProps(nextProps) {
this.createResponders(nextProps.disableGestures);
}
/**
* Change `top` style attribute
* Works only if `TopMenu` is a ref to React.Component
* @return {Void}
*/
updatePosition() {
this.TopMenu.setNativeProps({ top: this.top, });
}
/**
* Permission to use responder
* @return {Boolean} true
*/
handleMoveShouldSetPanResponder(e: Object, gestureState: Object) {
var x = Math.round(Math.abs(gestureState.dx));
var y = Math.round(Math.abs(gestureState.dy));
return x > this.props.toleranceX && y < this.props.toleranceY;
}
/**
* Handler on responder move
* @param {Synthetic Event} e
* @param {Object} gestureState
* @return {Void}
*/
handlePanResponderMove(e: Object, gestureState: Object) {
this.top = this.prevtop + gestureState.dx;
if ((this.menuPositionMultiplier() * this.top) > 0) {
this.updatePosition();
}
}
/**
* Returns 1 or -1 depending on the menuPosition
* @return {Number}
*/
menuPositionMultiplier() {
return this.props.menuPosition === 'bottom' ? -1 : 1;
}
/**
* Open menu
* @return {Void}
*/
openMenu() {
queueAnimation(this.props.animation);
this.top = this.menuPositionMultiplier() *
(this.props.openMenuOffset || openMenuOffset);
this.updatePosition();
this.prevtop = this.top;
if (!this.isOpen) {
this.props.onChange(this.isOpen);
this.isOpen = true;
// Force update to make the overlay appear (if touchToClose is set)
if (this.props.touchToClose) {
this.forceUpdate();
}
}
}
/**
* Close menu
* @return {Void}
*/
closeMenu() {
queueAnimation(this.props.animation);
this.top = this.menuPositionMultiplier() *
(this.props.hiddenMenuOffset || hiddenMenuOffset);
this.updatePosition();
this.prevtop = this.top;
if (this.isOpen) {
this.props.onChange(this.isOpen);
this.isOpen = false;
// Force update to make the overlay disappear (if touchToClose is set)
if (this.props.touchToClose) {
this.forceUpdate();
}
}
}
/**
* Toggle menu
* @return {Void}
*/
toggleMenu() {
if (this.isOpen) {
this.closeMenu();
} else {
this.openMenu();
}
}
/**
* Handler on responder move ending
* @param {Synthetic Event} e
* @param {Object} gestureState
* @return {Void}
*/
handlePanResponderEnd(e: Object, gestureState: Object) {
var shouldOpen = this.menuPositionMultiplier() *
(this.top + gestureState.dx);
if (shouldOpenMenu(shouldOpen)) {
this.openMenu();
} else {
this.closeMenu();
}
this.updatePosition();
this.prevtop = this.top;
}
handleOverlayPress(e: Object) {
this.closeMenu();
}
/**
* Get content view. This view will be rendered over menu
* @return {React.Component}
*/
getContentView() {
var getMenuActions = this.getMenuActions();
var overlay = null;
if (this.isOpen && this.props.touchToClose) {
overlay = (
<TouchableWithoutFeedback onPress={this.handleOverlayPress.bind(this)}>
<View style={styles.overlay} />
</TouchableWithoutFeedback>
);
}
var children = React.Children.map(this.props.children, (child) => {
return React.cloneElement(child, {
menuActions: getMenuActions,
});
});
return (
<View
style={styles.frontView}
ref={(TopMenu) => this.TopMenu = TopMenu}
{...this.responder.panHandlers}>
{children}
{overlay}
</View>
);
}
/**
* Get menu actions to expose it to
* menu and children components
* @return {Object} Public API methods
*/
getMenuActions() {
return {
close: this.closeMenu.bind(this),
toggle: this.toggleMenu.bind(this),
open: this.openMenu.bind(this),
};
}
/**
* Get menu view. This view will be rendered under
* content view. Also, this function will decorate
* passed `menu` component with Top menu API
* @return {React.Component}
*/
getMenuView() {
var menuActions = this.getMenuActions();
return (
<View style={styles.menu}>
{React.addons.cloneWithProps(this.props.menu, { menuActions, })}
</View>
);
}
/**
* Compose and render menu and content view
* @return {React.Component}
*/
render() {
return (
<View style={styles.container}>
{this.getContentView()}
</View>
);
}
}
TopMenu.propTypes = {
toleranceX: React.PropTypes.number,
toleranceY: React.PropTypes.number,
onChange: React.PropTypes.func,
touchToClose: React.PropTypes.bool,
};
TopMenu.defaultProps = {
toleranceY: 33,
toleranceX: -30,
onChange: noop,
touchToClose: false,
};
module.exports = TopMenu;
MAINMENU.js
'use strict';
var React = require('react-native');
var Menu = require('../../pages/MenuContent');
var SideMenu = require('react-native-side-menu');
// var Router = require('react-native-router');
var {
AppRegistry,
StyleSheet,
Text,
View,
Image,
ScrollView,
TouchableOpacity,
TouchableHighlight,
Component
} = React;
class Button extends Component {
handlePress(e) {
this.props.menuActions.toggle();
if (this.props.onPress) {
this.props.onPress(e);
}
}
render() {
return (
<TouchableOpacity
onPress={this.handlePress.bind(this)}>
<Text style={this.props.style}>{this.props.children}</Text>
</TouchableOpacity>
);
}
}
class MainMenu extends Component {
constructor(props) {
super(props);
this.state = {
touchToClose: false
};
}
handleOpenWithTouchToClose() {
this.setState({
touchToClose: false
});
}
handleChange(isOpen) {
if (!isOpen) {
this.setState({
touchToClose: false
});
}
}
render() {
return (
<SideMenu
menu={<Menu />}
touchToClose={this.state.touchToClose}
onChange={this.handleChange.bind(this)}>
<View style={styles.container}>
<Text style={styles.welcome}>
HLTHY
</Text>
<Text style={styles.menuItem}>
MENU ITEM 1
</Text>
<Text style={styles.separator}>
_____
</Text>
<Text style={styles.menuItem}>
MENU ITEM 2
</Text>
<Text style={styles.separator}>
_____
</Text>
<Text style={styles.menuItem}>
MENU ITEM 3
</Text>
<Text style={styles.separator}>
_____
</Text>
<Text style={styles.menuItem}>
MENU ITEM 4
</Text>
<Text style={styles.separator}>
_____
</Text>
<Text style={styles.menuItem}>
MENU ITEM 5
</Text>
</View>
<Button style={styles.toggleButton}>
HLTHY
</Button>
</SideMenu>
);
}
}
var styles = StyleSheet.create({
toggleButton: {
position: 'absolute',
bottom: 0,
backgroundColor: 'transparent',
color: 'white',
fontSize: 30,
borderRadius: 20,
marginLeft: -230,
marginTop: 30,
},
welcome: {
fontSize: 30,
textAlign: 'center',
margin: 10,
fontWeight: 'bold',
marginBottom: 30
},
menuItem: {
textAlign: 'center',
color: '#333333',
marginTop: 15,
fontSize: 20,
marginBottom: 15
},
separator: {
textAlign: 'center',
color: '#333333',
marginBottom: 15,
},
caption: {
fontSize: 20,
fontWeight: 'bold',
alignItems: 'center',
},
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'stretch',
backgroundColor: 'white',
marginLeft: -750,
marginTop: 0,
marginBottom: 30,
},
});
module.exports = MainMenu;