UNPKG

react-native-backdrop

Version:

Backdrop component built with Material Guidelines for React Native

356 lines (326 loc) 9.01 kB
import React, {Component} from 'react'; import { StyleSheet, Animated, View, SafeAreaView, TouchableOpacity, PanResponder, Dimensions, } from 'react-native'; const {height} = Dimensions.get('window'); const swipeConfigDefault = { velocityThreshold: 0.3, directionalOffsetThreshold: 50, }; const animationConfigDefault = { duration: 50, speed: 20, bounciness: 5, }; const isValidSwipe = ( velocity, velocityThreshold, directionalOffset, directionalOffsetThreshold, ) => Math.abs(velocity) > velocityThreshold && Math.abs(directionalOffset) < directionalOffsetThreshold; class Backdrop extends Component { static defaultProps = { onClose: () => {}, backdropStyle: {}, animationConfig: {}, swipeConfig: {}, overlayColor: 'rgba(0,0,0,0.32)', paddingBottom: 40, header: () => ( <View style={styles.closePlateContainer}> <View style={styles.closePlate} /> </View> ), loading: true, closedHeight: 0, }; state = { backdropHeight: 0, }; swipeConfig = {...swipeConfigDefault, ...this.props.swipeConfig}; animationConfig = { ...animationConfigDefault, ...this.props.animationConfig, }; _transitionY = new Animated.Value(this.props.closedHeight); componentDidUpdate(prevProps) { if (prevProps.visible !== this.props.visible && this.props.visible) { this.handleAnimationInit(); } } componentWillUnmount() { if (this.anim) { this.anim.stop(); this.anim = null; } } onLayout = event => { this._transitionY.setValue( event.nativeEvent.layout.height - this.props.closedHeight, ); this.setState( { backdropHeight: event.nativeEvent.layout.height, loading: false, }, () => { this.handleAnimationInit(); }, ); }; handleAnimationInit = () => { const spring = Animated.spring; const {backdropHeight} = this.state; if (this.anim) { this.anim = null; } const startAnimation = spring(this._transitionY, { toValue: 40, useNativeDriver: true, ...this.animationConfig, }); const closeAnimation = spring(this._transitionY, { toValue: backdropHeight - this.props.closedHeight, useNativeDriver: true, ...this.animationConfig, }); this.setState({ closeAnimation: closeAnimation, }); this.anim = startAnimation; if (this.props.visible) { this.anim.start(); } }; _panResponder = PanResponder.create({ onMoveShouldSetPanResponder: (evt, gestureState) => { if (gestureState.dy > 0 || gestureState.dy < 0) { return true; } }, onPanResponderMove: async (evt, gestureState) => { const {paddingBottom, closedHeight, visible} = this.props; const {backdropHeight} = this.state; const startingPosition = backdropHeight - closedHeight; const offset = visible ? backdropHeight - paddingBottom - (height - gestureState.y0) : closedHeight - (height - gestureState.y0); // Get the touch offset from top of backdrop if ( !visible && gestureState.dy < 0 && height - gestureState.moveY + offset <= startingPosition + closedHeight - paddingBottom ) { const newPosition = startingPosition + gestureState.dy; this._transitionY.setValue(newPosition); } else if ( visible && gestureState.dy > 0 && gestureState.moveY - offset < height - closedHeight ) { const newPosition = gestureState.dy + paddingBottom; this._transitionY.setValue(newPosition); } }, onPanResponderRelease: (evt, gestureState) => { const {paddingBottom, handleOpen, visible} = this.props; if (this._isValidVerticalSwipe(gestureState)) { if (gestureState.vy > 0) { this._handleClose(); } else { if (visible && this.anim) { this.anim.start(); } else { handleOpen(); if (this.anim) { this.anim.start(); } else { this.handleAnimationInit(); this.anim.start(); } } } } else { const {vy, dy} = gestureState; const {backdropHeight} = this.state; const halfHeight = dy > (backdropHeight - paddingBottom * 2) / 2; if (!visible) { handleOpen(); if (vy <= 0) { Animated.spring(this._transitionY, { toValue: 40, useNativeDriver: true, ...this.animationConfig, }).start(); } else { this._handleClose(); } } else { if (vy > 0 && halfHeight) { this._handleClose(); } else { Animated.spring(this._transitionY, { toValue: 40, useNativeDriver: true, ...this.animationConfig, }).start(); } } } }, onPanResponderTerminate: (evt, gestureState) => { this._handleClose(); }, }); _isValidVerticalSwipe(gestureState) { const {vy, dx} = gestureState; const {velocityThreshold, directionalOffsetThreshold} = this.swipeConfig; return isValidSwipe(vy, velocityThreshold, dx, directionalOffsetThreshold); } _handleClose = () => { const {onClose, handleClose} = this.props; if (handleClose) { this.props.handleClose(); } if (this.state.closeAnimation) { this.state.closeAnimation.start(() => { this.anim = null; this._transitionY.setValue( this.state.backdropHeight - this.props.closedHeight, ); if (onClose) { onClose(); } }); } else if (this.anim) { this.anim.stop(); this.anim = null; } }; render() { const { backdropStyle, visible, children, overlayColor, closedHeight, header, } = this.props; const {backdropHeight} = this.state; let opacityAnimation = backdropHeight ? this._transitionY.interpolate({ inputRange: [40, backdropHeight - closedHeight], outputRange: [1, 0], }) : 0; return ( <SafeAreaView pointerEvents="box-none" style={styles.wrapper}> <Animated.View style={[ styles.overlayStyle, { backgroundColor: overlayColor, opacity: opacityAnimation, }, ]} pointerEvents={visible ? 'auto' : 'none'}> <TouchableOpacity style={styles.overlayTouchable} onPress={this._handleClose} /> </Animated.View> <Animated.View pointerEvents="box-none" accessibilityLiveRegion="polite" style={[ styles.contentContainer, { opacity: backdropHeight ? 1 : 0, // Hide before layout prevents blink transform: [ { translateY: this._transitionY, }, ], }, ]}> <View pointerEvents={backdropHeight ? 'auto' : 'none'} style={[ styles.container, {paddingBottom: this.props.paddingBottom + 12}, backdropStyle, ]} onLayout={this.onLayout} {...this._panResponder.panHandlers}> {!!header && header()} <View>{children}</View> </View> </Animated.View> </SafeAreaView> ); } } const styles = StyleSheet.create({ wrapper: { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, zIndex: 100, elevation: 10, justifyContent: 'flex-end', flex: 1, paddingTop: 40, }, container: { elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 1, }, shadowOpacity: 0.2, shadowRadius: 1.41, backgroundColor: '#ffffff', borderTopLeftRadius: 20, borderTopRightRadius: 20, paddingHorizontal: 16, }, overlayTouchable: { flex: 1, }, contentContainer: { marginTop: 48, flex: 1, justifyContent: 'flex-end', }, overlayStyle: { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, }, closePlateContainer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', height: 32, }, closePlate: { width: 40, height: 5, borderRadius: 5, backgroundColor: '#bdbdbd', }, }); export default Backdrop;