Skip to content

Instantly share code, notes, and snippets.

@danielgindi
Created November 1, 2018 14:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save danielgindi/922ef5550c6f0de002e469dbd2a49b37 to your computer and use it in GitHub Desktop.
Save danielgindi/922ef5550c6f0de002e469dbd2a49b37 to your computer and use it in GitHub Desktop.
Callout view for React Native
import React, { Component } from 'react';
import { View, StyleSheet, Text, Image } from 'react-native';
import PropTypes from 'prop-types';
import { TouchableNativeFeedback } from 'react-native';
export default class CalloutView extends Component {
static propTypes = {
onPress: PropTypes.func,
parentLayout: PropTypes.any,
position: PropTypes.shape({
x: PropTypes.number,
y: PropTypes.number
}),
fenceSize: PropTypes.shape({
width: PropTypes.number,
height: PropTypes.number
}),
arrowColor: PropTypes.string,
arrowSize: PropTypes.number,
};
static defaultProps = {
arrowSize: 9,
arrowColor: '#333',
};
constructor(props) {
super(props);
this.state = {
isLayingOut: true,
};
}
onLayout(e) {
let isNewLayout = !this.state.layout ||
this.state.layout.width !== e.nativeEvent.layout.width ||
this.state.layout.height !== e.nativeEvent.layout.height;
this.state.layout = e.nativeEvent.layout;
if (this.props.position) {
if (this.state.layout.width !== this.props.parentLayout.width || !isNewLayout) {
this.setState({
isLayingOut: false,
});
}
else if (isNewLayout) {
this.forceUpdate();
}
}
}
render() {
let positionStyle = {};
let mainLayersStyle = [styles.mainLayers];
let beforeArrowStyle = styles.none;
let afterArrowStyle = styles.none;
let layout = this.state.layout,
parentLayout = this.props.parentLayout,
position = this.props.position,
fenceRadius = {
width: (this.props.fenceSize && this.props.fenceSize.width || 0) / 2,
height: (this.props.fenceSize && this.props.fenceSize.height || 0) / 2,
};
let arrowSize = this.props.arrowSize;
let arrowColor = this.props.arrowColor;
let arrowStyle = {
borderLeftWidth: arrowSize,
borderRightWidth: arrowSize,
borderBottomWidth: arrowSize,
borderTopWidth: arrowSize,
borderLeftColor: 'transparent',
borderRightColor: 'transparent',
borderBottomColor: 'transparent',
borderTopColor: 'transparent',
};
if (layout && position) {
let xRel = position.x / parentLayout.width;
positionStyle.position = 'absolute';
if ((xRel < 0.2 || xRel > 0.8) && layout.width + fenceRadius.width < parentLayout.width * 0.8) {
mainLayersStyle.push(styles.mainLayersHorz);
if (xRel < 0.2) {
positionStyle.left = Math.min(position.x + fenceRadius.width, parentLayout.width - layout.width);
positionStyle.top = Math.max(Math.min(position.y - layout.height / 2, parentLayout.height - layout.height), 0);
arrowStyle.borderLeftWidth = 0;
arrowStyle.borderRightColor = arrowColor;
arrowStyle.transform = [{translateX: 2}, {translateY: -arrowSize}];
beforeArrowStyle = [styles.calloutArrow, arrowStyle, {
top: position.y - positionStyle.top
}];
} else {
positionStyle.left = Math.max(position.x - layout.width - fenceRadius.width, 0);
positionStyle.top = Math.max(Math.min(position.y - layout.height / 2, parentLayout.height - layout.height), 0);
arrowStyle.borderRightWidth = 0;
arrowStyle.borderLeftColor = arrowColor;
arrowStyle.transform = [{translateX: -2}, {translateY: -arrowSize}];
afterArrowStyle = [styles.calloutArrow, arrowStyle, {
top: position.y - positionStyle.top
}];
}
} else {
if (position.y < parentLayout.height / 2 - layout.height - fenceRadius.height) {
positionStyle.top = Math.min(position.y + fenceRadius.height, parentLayout.height - layout.height);
positionStyle.left = Math.max(Math.min(position.x - layout.width / 2, parentLayout.width - layout.width), 0);
arrowStyle.borderTopWidth = 0;
arrowStyle.borderBottomColor = arrowColor;
arrowStyle.transform = [{translateX: -arrowSize}, {translateY: 2}];
beforeArrowStyle = [styles.calloutArrow, arrowStyle, {
left: position.x - positionStyle.left
}];
} else {
positionStyle.top = Math.max(position.y - layout.height - fenceRadius.height, 0);
positionStyle.left = Math.max(Math.min(position.x - layout.width / 2, parentLayout.width - layout.width), 0);
arrowStyle.borderBottomWidth = 0;
arrowStyle.borderTopColor = arrowColor;
arrowStyle.transform = [{translateX: -arrowSize}, {translateY: -2}];
afterArrowStyle = [styles.calloutArrow, arrowStyle, {
left: position.x - positionStyle.left
}];
}
}
}
if (position && this.state.isLayingOut) {
positionStyle.opacity = 0;
}
let mainStyle = Array.isArray(this.props.style)
? [styles.mainWrapper, positionStyle].concat(this.props.style)
: [styles.mainWrapper, positionStyle, this.props.style || null];
let isButton = !!this.props.onPress;
let renderedInner = (
<View style={mainLayersStyle}>
<View style={beforeArrowStyle} />
<View style={styles.main}>
{this.props.children}
</View>
<View style={afterArrowStyle} />
</View>
);
if (isButton) {
return (
<TouchableNativeFeedback
style={mainStyle}
onLayout={this.onLayout.bind(this)}
onPress={this.props.onPress}>
{renderedInner}
</TouchableNativeFeedback>
);
} else {
return (
<View
style={mainStyle}
onLayout={this.onLayout.bind(this)}
onPress={this.props.onPress}>
{renderedInner}
</View>
)
}
}
}
const styles = StyleSheet.create({
mainWrapper: {
backgroundColor: 'transparent',
display: 'flex',
flexGrow: 0,
flexShrink: 0,
shadowColor: 'rgba(0,0,0,0.36)',
shadowOffset: { width: 0, height: 2 },
shadowRadius: 9,
shadowOpacity: 1,
elevation: 1,
},
mainLayers: {
display: 'flex',
flexGrow: 0,
flexShrink: 0,
flexDirection: 'column',
},
mainLayersHorz: {
flexDirection: 'row',
},
main: {
zIndex: 1,
display: 'flex',
flexGrow: 0,
flexShrink: 0,
flexDirection: 'column',
},
none: {
display: 'none',
},
calloutArrow: {
zIndex: 0,
width: 0,
height: 0,
backgroundColor: 'transparent',
borderStyle: 'solid',
},
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment