UNPKG

@ohmi/react-native-safe-area-view

Version:

JS only version of SafeAreaView for supporting iPhone X safe area insets.

398 lines (317 loc) 9.7 kB
import React, { Component } from 'react'; import { Dimensions, InteractionManager, Platform, StyleSheet, Animated, StatusBar, } from 'react-native'; import hoistStatics from 'hoist-non-react-statics'; import withOrientation from './withOrientation'; // See https://mydevice.io/devices/ for device dimensions const X_WIDTH = 375; const X_HEIGHT = 812; const XSMAX_WIDTH = 414; const XSMAX_HEIGHT = 896; const PAD_WIDTH = 768; const PAD_HEIGHT = 1024; // const { height: D_HEIGHT, width: D_WIDTH } = Dimensions.get('window'); const getResolvedDimensions = () => { const { width, height } = Dimensions.get('window'); if (width === 0 && height === 0) return Dimensions.get('screen'); return { width, height }; }; const { height: D_HEIGHT, width: D_WIDTH } = getResolvedDimensions(); const isIPhoneX = (() => { if (Platform.OS === 'web') return false; return ( Platform.OS === 'ios' && ((D_HEIGHT === X_HEIGHT && D_WIDTH === X_WIDTH) || (D_HEIGHT === X_WIDTH && D_WIDTH === X_HEIGHT)) || ((D_HEIGHT === XSMAX_HEIGHT && D_WIDTH === XSMAX_WIDTH) || (D_HEIGHT === XSMAX_WIDTH && D_WIDTH === XSMAX_HEIGHT)) ); })(); const isIPad = (() => { if (Platform.OS !== 'ios' || isIPhoneX) return false; if (D_HEIGHT > D_WIDTH && D_WIDTH < PAD_WIDTH) { return false; } if (D_WIDTH > D_HEIGHT && D_HEIGHT < PAD_WIDTH) { return false; } return true; })(); let _customStatusBarHeight = null; const statusBarHeight = (isLandscape, forceInset) => { if (_customStatusBarHeight !== null) { return _customStatusBarHeight; } if (Platform.OS === 'android') { if (global.Expo) { return global.Expo.Constants.statusBarHeight; } else { return 0; } } if (Platform.OS === 'harmony') { if (forceInset?.top === 'never') { return 0; } else { return isLandscape ? 0 : 35; } } if (isIPhoneX) { return isLandscape ? 0 : 44; } if (isIPad) { return 20; } return isLandscape ? 0 : 20; }; const doubleFromPercentString = percent => { if (!percent.includes('%')) { return 0; } const dbl = parseFloat(percent) / 100; if (isNaN(dbl)) return 0; return dbl; }; class SafeView extends Component { static setStatusBarHeight = height => { _customStatusBarHeight = height; }; state = { touchesTop: true, touchesBottom: true, touchesLeft: true, touchesRight: true, orientation: null, viewWidth: 0, viewHeight: 0, }; componentDidMount() { this._isMounted = true; InteractionManager.runAfterInteractions(() => { this._onLayout(); }); } componentWillUnmount() { this._isMounted = false; } componentWillReceiveProps() { this._onLayout(); } render() { const { forceInset = false, isLandscape, style, showTop, ...props } = this.props; const safeAreaStyle = this._getSafeAreaStyle(); return ( <Animated.View ref={c => (this.view = c)} pointerEvents='box-none' {...props} onLayout={this._onLayout} style={safeAreaStyle} /> ); } _onLayout = (...args) => { if (!this._isMounted) return; if (!this.view) return; const { isLandscape } = this.props; const { orientation } = this.state; const newOrientation = isLandscape ? 'landscape' : 'portrait'; const insets = this.props.forceInset || {}; if (orientation && orientation === newOrientation) { return; } const { width: WIDTH, height: HEIGHT } = getResolvedDimensions(); // getNode() is not necessary in newer versions of React Native const node = typeof this.view.measureInWindow === 'function' ? this.view : this.view.getNode(); node.measureInWindow((winX, winY, winWidth, winHeight) => { if (!this.view) { return; } let realY = winY; let realX = winX; if (realY >= HEIGHT) { realY = realY % HEIGHT; } else if (realY < 0) { realY = (realY % HEIGHT) + HEIGHT; } if (realX >= WIDTH) { realX = realX % WIDTH; } else if (realX < 0) { realX = (realX % WIDTH) + WIDTH; } const touchesTop = realY === 0; const touchesBottom = realY + winHeight >= HEIGHT; const touchesLeft = realX === 0; const touchesRight = realX + winWidth >= WIDTH; this.setState({ touchesTop, touchesBottom, touchesLeft, touchesRight, orientation: newOrientation, viewWidth: winWidth, viewHeight: winHeight, }); }); }; // this.view._component.measureInWindow((winX, winY, winWidth, winHeight) => { // if (!this.view) { // return; // } // let realY = winY; // let realX = winX; // if (realY >= HEIGHT) { // realY = realY % HEIGHT; // } else if (realY < 0) { // realY = realY % HEIGHT + HEIGHT; // } // if (realX >= WIDTH) { // realX = realX % WIDTH; // } else if (realX < 0) { // realX = realX % WIDTH + WIDTH; // } // const touchesTop = realY === 0; // const touchesBottom = realY + winHeight >= HEIGHT; // const touchesLeft = realX === 0; // const touchesRight = realX + winWidth >= WIDTH; // this.setState({ // touchesTop, // touchesBottom, // touchesLeft, // touchesRight, // orientation: newOrientation, // viewWidth: winWidth, // viewHeight: winHeight, // }); // if (this.props.onLayout) this.props.onLayout(...args); // }); // }; _getSafeAreaStyle = () => { const { touchesTop, touchesBottom, touchesLeft, touchesRight } = this.state; const { forceInset, isLandscape } = this.props; const { paddingTop, paddingBottom, paddingLeft, paddingRight, viewStyle, } = this._getViewStyles(); if (viewStyle.height > StatusBar.currentHeight) { viewStyle.height = StatusBar.currentHeight + 20 } const style = { ...viewStyle, paddingTop: touchesTop ? this._getInset('top') : 0, paddingBottom: touchesBottom ? this._getInset('bottom') : 0, paddingLeft: touchesLeft ? this._getInset('left') : 0, paddingRight: touchesRight ? this._getInset('right') : 0, marginTop: Platform.OS === 'harmony' ? this._getInset('top') : 0, }; if (forceInset) { Object.keys(forceInset).forEach(key => { let inset = forceInset[key]; if (inset === 'always') { inset = this._getInset(key); } if (inset === 'never') { inset = 0; } switch (key) { case 'horizontal': { style.paddingLeft = inset; style.paddingRight = inset; break; } case 'vertical': { style.paddingTop = inset; style.paddingBottom = inset; break; } case 'left': case 'right': case 'top': case 'bottom': { const padding = `padding${key[0].toUpperCase()}${key.slice(1)}`; style[padding] = inset; break; } } }); } return style; }; _getViewStyles = () => { const { viewWidth } = this.state; // get padding values from style to add back in after insets are determined // default precedence: padding[Side] -> vertical | horizontal -> padding -> 0 let { padding = 0, paddingVertical = padding, paddingHorizontal = padding, paddingTop = paddingVertical, paddingBottom = paddingVertical, paddingLeft = paddingHorizontal, paddingRight = paddingHorizontal, ...viewStyle } = StyleSheet.flatten(this.props.style || {}); if (typeof paddingTop !== 'number') { paddingTop = doubleFromPercentString(paddingTop) * viewWidth; } if (typeof paddingBottom !== 'number') { paddingBottom = doubleFromPercentString(paddingBottom) * viewWidth; } if (typeof paddingLeft !== 'number') { paddingLeft = doubleFromPercentString(paddingLeft) * viewWidth; } if (typeof paddingRight !== 'number') { paddingRight = doubleFromPercentString(paddingRight) * viewWidth; } return { paddingTop, paddingBottom, paddingLeft, paddingRight, viewStyle, }; }; _getInset = key => { const { isLandscape, forceInset } = this.props; switch (key) { case 'horizontal': case 'right': case 'left': { return isLandscape ? (isIPhoneX ? 44 : 0) : 0; } case 'vertical': case 'top': { return statusBarHeight(isLandscape, forceInset); } case 'bottom': { return isIPhoneX ? (isLandscape ? 24 : 34) : 0; } } }; } const SafeAreaView = withOrientation(SafeView); export default SafeAreaView; const withSafeArea = function (forceInset = {}) { return (WrappedComponent) => { class withSafeArea extends Component { render() { return ( <SafeAreaView style={{ flex: 1 }} forceInset={forceInset}> <WrappedComponent {...this.props} /> </SafeAreaView> ); } } return hoistStatics(withSafeArea, WrappedComponent); }; } export { withSafeArea };