react-native-enhanced-toast
Version:
react native `enhanced` toast like component, pure javascript solution
300 lines (276 loc) • 7.72 kB
JavaScript
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import {
ViewPropTypes,
StyleSheet,
View,
Text,
Animated,
Dimensions,
TouchableWithoutFeedback,
Easing,
Keyboard
} from 'react-native'
import EvilIcons from 'react-native-vector-icons/EvilIcons'
const TOAST_MAX_WIDTH = 0.8
const TOAST_ANIMATION_DURATION = 200
const positions = {
TOP: 20,
BOTTOM: -20,
CENTER: 0
}
const durations = {
LONG: 3500,
SHORT: 2000
}
const styles = StyleSheet.create({
defaultStyle: {
position: 'absolute',
left: 0,
right: 0,
justifyContent: 'center',
alignItems: 'center'
},
containerStyle: {
flexDirection: 'row',
alignItems: 'center',
padding: 10,
backgroundColor: '#000',
opacity: 0.8,
borderRadius: 5
},
shadowStyle: {
shadowColor: '#000',
shadowOffset: {
width: 4,
height: 4
},
shadowOpacity: 0.8,
shadowRadius: 6,
elevation: 10
},
textStyle: {
fontSize: 16,
color: '#fff',
textAlign: 'center'
}
})
class ToastContainer extends Component {
static displayName = 'ToastContainer'
static propTypes = {
...ViewPropTypes,
containerStyle: ViewPropTypes.style,
duration: PropTypes.number,
visible: PropTypes.bool,
position: PropTypes.number,
animation: PropTypes.bool,
shadow: PropTypes.bool,
backgroundColor: PropTypes.string,
opacity: PropTypes.number,
shadowColor: PropTypes.string,
textColor: PropTypes.string,
textStyle: Text.propTypes.style,
closeSize: PropTypes.number,
closeColor: PropTypes.string,
closeStyle: ViewPropTypes.style,
delay: PropTypes.number,
hideOnPress: PropTypes.bool,
showCloseButton: PropTypes.bool,
onPressMessage: PropTypes.func,
onHide: PropTypes.func,
onHidden: PropTypes.func,
onShow: PropTypes.func,
onShown: PropTypes.func
}
static defaultProps = {
containerStyle: {},
visible: false,
duration: durations.SHORT,
animation: true,
shadow: true,
position: positions.BOTTOM,
opacity: 0.8,
delay: 0,
hideOnPress: true,
showCloseButton: false,
onPressMessage: null,
closeSize: 24,
closeColor: 'black',
closeStyle: {
opacity: 0.8,
marginLeft: 5
},
onHide: null,
onHidden: null,
onShow: null,
onShown: null
}
constructor(props) {
super(props)
const window = Dimensions.get('window')
this.state = {
visible: this.props.visible,
opacity: new Animated.Value(0),
windowWidth: window.width,
windowHeight: window.height,
keyboardScreenY: window.height
}
}
componentWillMount() {
Dimensions.addEventListener('change', this._windowChanged)
Keyboard.addListener('keyboardDidChangeFrame', this._keyboardDidChangeFrame)
}
componentDidMount = () => {
if (this.state.visible) {
this._showTimeout = setTimeout(() => this._show(), this.props.delay)
}
}
componentWillReceiveProps = nextProps => {
if (nextProps.visible !== this.props.visible) {
if (nextProps.visible) {
clearTimeout(this._showTimeout)
clearTimeout(this._hideTimeout)
this._showTimeout = setTimeout(() => this._show(), this.props.delay)
} else {
this._hide()
}
this.setState({
visible: nextProps.visible
})
}
}
componentWillUpdate() {
const { windowHeight, keyboardScreenY } = this.state
this._keyboardHeight = Math.max(windowHeight - keyboardScreenY, 0)
}
componentWillUnmount = () => {
Dimensions.removeEventListener('change', this._windowChanged)
Keyboard.removeListener('keyboardDidChangeFrame', this._keyboardDidChangeFrame)
this._hide()
}
_animating = false
_root = null
_hideTimeout = null
_showTimeout = null
_keyboardHeight = 0
_windowChanged = ({ window }) => {
this.setState({
windowWidth: window.width,
windowHeight: window.height
})
}
_keyboardDidChangeFrame = ({ endCoordinates }) => {
this.setState({
keyboardScreenY: endCoordinates.screenY
})
}
_show = () => {
clearTimeout(this._showTimeout)
if (!this._animating) {
clearTimeout(this._hideTimeout)
this._animating = true
this._root.setNativeProps({
pointerEvents: 'auto'
})
this.props.onShow && this.props.onShow(this.props.siblingManager)
Animated.timing(this.state.opacity, {
toValue: this.props.containerStyle.opacity || this.props.opacity,
duration: this.props.animation ? TOAST_ANIMATION_DURATION : 0,
easing: Easing.out(Easing.ease)
}).start(({ finished }) => {
if (finished) {
this._animating = false
this.props.onShown && this.props.onShown(this.props.siblingManager)
if (this.props.duration > 0) {
this._hideTimeout = setTimeout(() => this._hide(), this.props.duration)
}
}
})
}
}
_hide = () => {
clearTimeout(this._showTimeout)
clearTimeout(this._hideTimeout)
if (!this._animating) {
this._root.setNativeProps({
pointerEvents: 'none'
})
this.props.onHide && this.props.onHide(this.props.siblingManager)
Animated.timing(this.state.opacity, {
toValue: 0,
duration: this.props.animation ? TOAST_ANIMATION_DURATION : 0,
easing: Easing.in(Easing.ease)
}).start(({ finished }) => {
if (finished) {
this._animating = false
this.props.onHidden && this.props.onHidden(this.props.siblingManager)
}
})
}
}
_onPressMessage = () => {
const { onPressMessage, hideOnPress } = this.props
if (onPressMessage) {
onPressMessage()
this._hide()
} else if (hideOnPress) {
this._hide()
}
}
render() {
const { props } = this
const {
showCloseButton, closeSize, closeColor, closeStyle
} = this.props
const { windowWidth } = this.state
const offset = props.position
const position = offset
? {
[offset < 0 ? 'bottom' : 'top']: offset < 0 ? this._keyboardHeight - offset : offset
}
: {
top: 0,
bottom: this._keyboardHeight
}
return this.state.visible || this._animating ? (
<View style={[styles.defaultStyle, position]} pointerEvents="box-none">
<TouchableWithoutFeedback onPress={this._onPressMessage}>
<Animated.View
style={[
styles.containerStyle,
{ marginHorizontal: windowWidth * ((1 - TOAST_MAX_WIDTH) / 2) },
props.containerStyle,
props.backgroundColor && { backgroundColor: props.backgroundColor },
{
opacity: this.state.opacity
},
props.shadow && styles.shadowStyle,
props.shadowColor && { shadowColor: props.shadowColor }
]}
pointerEvents="none"
ref={ele => {
this._root = ele
}}
>
<Text
style={[
styles.textStyle,
props.textStyle,
props.textColor && { color: props.textColor }
]}
>
{this.props.children}
</Text>
{showCloseButton && (
<TouchableWithoutFeedback onPress={this._hide}>
<EvilIcons name="close" size={closeSize} color={closeColor} style={closeStyle} />
</TouchableWithoutFeedback>
)}
</Animated.View>
</TouchableWithoutFeedback>
</View>
) : null
}
}
export default ToastContainer
export { positions, durations }