diff --git a/android/app/src/main/java/com/expensify/chat/MainActivity.java b/android/app/src/main/java/com/expensify/chat/MainActivity.java index b4eb483f8de6..ff4945f7f71f 100644 --- a/android/app/src/main/java/com/expensify/chat/MainActivity.java +++ b/android/app/src/main/java/com/expensify/chat/MainActivity.java @@ -3,6 +3,9 @@ import android.os.Bundle; import android.content.pm.ActivityInfo; import android.view.KeyEvent; +import android.view.View; +import android.view.WindowInsets; + import com.expensify.chat.bootsplash.BootSplash; import com.expensify.reactnativekeycommand.KeyCommandModule; import com.facebook.react.ReactActivity; @@ -45,6 +48,19 @@ protected void onCreate(Bundle savedInstanceState) { if (getResources().getBoolean(R.bool.portrait_only)) { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); } + + // Sets translucent status bar. This code is based on what the react-native StatusBar + // module does, but we need to do it here to avoid the splash screen jumping on app start. + View decorView = getWindow().getDecorView(); + decorView.setOnApplyWindowInsetsListener( + (v, insets) -> { + WindowInsets defaultInsets = v.onApplyWindowInsets(insets); + return defaultInsets.replaceSystemWindowInsets( + defaultInsets.getSystemWindowInsetLeft(), + 0, + defaultInsets.getSystemWindowInsetRight(), + defaultInsets.getSystemWindowInsetBottom()); + }); } /** @@ -54,15 +70,15 @@ protected void onCreate(Bundle savedInstanceState) { @Override public boolean onKeyDown(int keyCode, KeyEvent event) { // Disabling hardware ESCAPE support which is handled by Android - if (event.getKeyCode() == KeyEvent.KEYCODE_ESCAPE) { - return false; + if (event.getKeyCode() == KeyEvent.KEYCODE_ESCAPE) { + return false; } KeyCommandModule.getInstance().onKeyDownEvent(keyCode, event); return super.onKeyDown(keyCode, event); } @Override - public boolean onKeyLongPress(int keyCode, KeyEvent event) { + public boolean onKeyLongPress(int keyCode, KeyEvent event) { // Disabling hardware ESCAPE support which is handled by Android if (event.getKeyCode() == KeyEvent.KEYCODE_ESCAPE) { return false; } KeyCommandModule.getInstance().onKeyDownEvent(keyCode, event); @@ -70,10 +86,10 @@ public boolean onKeyLongPress(int keyCode, KeyEvent event) { } @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { + public boolean onKeyUp(int keyCode, KeyEvent event) { // Disabling hardware ESCAPE support which is handled by Android if (event.getKeyCode() == KeyEvent.KEYCODE_ESCAPE) { return false; } KeyCommandModule.getInstance().onKeyDownEvent(keyCode, event); return super.onKeyUp(keyCode, event); } -} \ No newline at end of file +} diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml index 07a41cec581f..b4d8c2181b0b 100644 --- a/android/app/src/main/res/values/colors.xml +++ b/android/app/src/main/res/values/colors.xml @@ -1,6 +1,5 @@ #03D47C - #061B09 #FFFFFF #03D47C #0b1b34 diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index c789cdfef09f..34d33d240458 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -6,7 +6,7 @@ diff --git a/src/components/CustomStatusBar/index.android.js b/src/components/CustomStatusBar/index.android.js index fa3f8921e82a..a7bf509114e6 100644 --- a/src/components/CustomStatusBar/index.android.js +++ b/src/components/CustomStatusBar/index.android.js @@ -1,18 +1,10 @@ -import React from 'react'; -import {StatusBar} from 'react-native'; -import themeColors from '../../styles/themes/default'; - /** - * Only the Android platform supports "setBackgroundColor" + * On Android we setup the status bar in native code. */ -export default class CustomStatusBar extends React.Component { - componentDidMount() { - StatusBar.setBarStyle('light-content'); - StatusBar.setBackgroundColor(themeColors.appBG); - } - - render() { - return ; - } +export default function CustomStatusBar() { + // Prefer to not render the StatusBar component in Android as it can cause + // issues with edge to edge display. We setup the status bar appearance in + // MainActivity.java and styles.xml. + return null; } diff --git a/src/components/Modal/BaseModal.js b/src/components/Modal/BaseModal.js index 710743bb0edb..7dac263e453c 100644 --- a/src/components/Modal/BaseModal.js +++ b/src/components/Modal/BaseModal.js @@ -1,5 +1,5 @@ import React, {PureComponent} from 'react'; -import {StatusBar, View} from 'react-native'; +import {View} from 'react-native'; import PropTypes from 'prop-types'; import ReactNativeModal from 'react-native-modal'; import {SafeAreaInsetsContext} from 'react-native-safe-area-context'; @@ -127,9 +127,7 @@ class BaseModal extends PureComponent { hasBackdrop={this.props.fullscreen} coverScreen={this.props.fullscreen} style={modalStyle} - // When `statusBarTranslucent` is true on Android, the modal fully covers the status bar. - // Since `windowHeight` doesn't include status bar height, it should be added in the `deviceHeight` calculation. - deviceHeight={this.props.windowHeight + ((this.props.statusBarTranslucent && StatusBar.currentHeight) || 0)} + deviceHeight={this.props.windowHeight} deviceWidth={this.props.windowWidth} animationIn={this.props.animationIn || animationIn} animationOut={this.props.animationOut || animationOut} @@ -147,7 +145,7 @@ class BaseModal extends PureComponent { paddingBottom: safeAreaPaddingBottom, paddingLeft: safeAreaPaddingLeft, paddingRight: safeAreaPaddingRight, - } = StyleUtils.getSafeAreaPadding(insets, this.props.statusBarTranslucent); + } = StyleUtils.getSafeAreaPadding(insets); const modalPaddingStyles = StyleUtils.getModalPaddingStyles({ safeAreaPaddingTop, diff --git a/src/components/SplashScreenHider/index.native.js b/src/components/SplashScreenHider/index.native.js index 8e544b7da2a1..6b626ceaaf1f 100644 --- a/src/components/SplashScreenHider/index.native.js +++ b/src/components/SplashScreenHider/index.native.js @@ -1,6 +1,6 @@ import {useCallback, useRef} from 'react'; import PropTypes from 'prop-types'; -import {StatusBar, StyleSheet} from 'react-native'; +import {StyleSheet} from 'react-native'; import Reanimated, {useSharedValue, withTiming, Easing, useAnimatedStyle, runOnJS} from 'react-native-reanimated'; import BootSplash from '../../libs/BootSplash'; import Logo from '../../../assets/images/new-expensify-dark.svg'; @@ -64,7 +64,6 @@ const SplashScreenHider = (props) => { opacityStyle, { // Apply negative margins to center the logo on window (instead of screen) - marginTop: -(StatusBar.currentHeight || 0), marginBottom: -(BootSplash.navigationBarHeight || 0), }, ]} diff --git a/src/components/withWindowDimensions.js b/src/components/withWindowDimensions.js index 109ee2dfe88a..14ca3572c482 100644 --- a/src/components/withWindowDimensions.js +++ b/src/components/withWindowDimensions.js @@ -1,9 +1,10 @@ -/* eslint-disable react/no-unused-state */ import React, {forwardRef, createContext} from 'react'; import PropTypes from 'prop-types'; import {Dimensions} from 'react-native'; +import {SafeAreaInsetsContext} from 'react-native-safe-area-context'; import getComponentDisplayName from '../libs/getComponentDisplayName'; import variables from '../styles/variables'; +import getWindowHeightAdjustment from '../libs/getWindowHeightAdjustment'; const WindowDimensionsContext = createContext(null); const windowDimensionsPropTypes = { @@ -16,7 +17,7 @@ const windowDimensionsPropTypes = { // Is the window width narrow, like on a mobile device? isSmallScreenWidth: PropTypes.bool.isRequired, - // Is the window width narrow, like on a tablet device? + // Is the window width medium sized, like on a tablet device? isMediumScreenWidth: PropTypes.bool.isRequired, // Is the window width wide, like on a browser or desktop? @@ -35,18 +36,12 @@ class WindowDimensionsProvider extends React.Component { this.onDimensionChange = this.onDimensionChange.bind(this); const initialDimensions = Dimensions.get('window'); - const isSmallScreenWidth = initialDimensions.width <= variables.mobileResponsiveWidthBreakpoint; - const isMediumScreenWidth = initialDimensions.width > variables.mobileResponsiveWidthBreakpoint && initialDimensions.width <= variables.tabletResponsiveWidthBreakpoint; - const isLargeScreenWidth = !isSmallScreenWidth && !isMediumScreenWidth; this.dimensionsEventListener = null; this.state = { windowHeight: initialDimensions.height, windowWidth: initialDimensions.width, - isSmallScreenWidth, - isMediumScreenWidth, - isLargeScreenWidth, }; } @@ -69,20 +64,36 @@ class WindowDimensionsProvider extends React.Component { */ onDimensionChange(newDimensions) { const {window} = newDimensions; - const isSmallScreenWidth = window.width <= variables.mobileResponsiveWidthBreakpoint; - const isMediumScreenWidth = !isSmallScreenWidth && window.width <= variables.tabletResponsiveWidthBreakpoint; - const isLargeScreenWidth = !isSmallScreenWidth && !isMediumScreenWidth; + this.setState({ windowHeight: window.height, windowWidth: window.width, - isSmallScreenWidth, - isMediumScreenWidth, - isLargeScreenWidth, }); } render() { - return {this.props.children}; + return ( + + {(insets) => { + const isSmallScreenWidth = this.state.windowWidth <= variables.mobileResponsiveWidthBreakpoint; + const isMediumScreenWidth = !isSmallScreenWidth && this.state.windowWidth <= variables.tabletResponsiveWidthBreakpoint; + const isLargeScreenWidth = !isSmallScreenWidth && !isMediumScreenWidth; + return ( + + {this.props.children} + + ); + }} + + ); } } diff --git a/src/libs/getSafeAreaPaddingTop/index.android.js b/src/libs/getSafeAreaPaddingTop/index.android.js deleted file mode 100644 index db66c1739ffb..000000000000 --- a/src/libs/getSafeAreaPaddingTop/index.android.js +++ /dev/null @@ -1,12 +0,0 @@ -import {StatusBar} from 'react-native'; - -/** - * Returns safe area padding top to use for a View - * - * @param {Object} insets - * @param {Boolean} statusBarTranslucent - * @returns {Number} - */ -export default function getSafeAreaPaddingTop(insets, statusBarTranslucent) { - return (statusBarTranslucent && StatusBar.currentHeight) || 0; -} diff --git a/src/libs/getSafeAreaPaddingTop/index.js b/src/libs/getSafeAreaPaddingTop/index.js deleted file mode 100644 index 89b3579587e7..000000000000 --- a/src/libs/getSafeAreaPaddingTop/index.js +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Takes safe area insets and returns padding top to use for a View - * - * @param {Object} insets - * @returns {Number} - */ -export default function getSafeAreaPaddingTop(insets) { - return insets.top; -} diff --git a/src/libs/getWindowHeightAdjustment/index.android.js b/src/libs/getWindowHeightAdjustment/index.android.js new file mode 100644 index 000000000000..b360467d31c1 --- /dev/null +++ b/src/libs/getWindowHeightAdjustment/index.android.js @@ -0,0 +1,4 @@ +// On Android the window height does not include the status bar height, so we need to add it manually. +export default function getWindowHeightAdjustment(insets) { + return insets.top; +} diff --git a/src/libs/getWindowHeightAdjustment/index.js b/src/libs/getWindowHeightAdjustment/index.js new file mode 100644 index 000000000000..9ddd1e7cefee --- /dev/null +++ b/src/libs/getWindowHeightAdjustment/index.js @@ -0,0 +1,4 @@ +// Some platforms need to adjust the window height. +export default function getWindowHeightAdjustment() { + return 0; +} diff --git a/src/pages/signin/SignInPage.js b/src/pages/signin/SignInPage.js index ac1759661ff1..624d0f2a8b65 100644 --- a/src/pages/signin/SignInPage.js +++ b/src/pages/signin/SignInPage.js @@ -1,9 +1,10 @@ import React, {Component} from 'react'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; +import {View} from 'react-native'; import lodashGet from 'lodash/get'; import Str from 'expensify-common/lib/str'; -import {SafeAreaView} from 'react-native-safe-area-context'; +import {withSafeAreaInsets} from 'react-native-safe-area-context'; import ONYXKEYS from '../../ONYXKEYS'; import styles from '../../styles/styles'; import compose from '../../libs/compose'; @@ -19,6 +20,7 @@ import Permissions from '../../libs/Permissions'; import UnlinkLoginForm from './UnlinkLoginForm'; import withWindowDimensions, {windowDimensionsPropTypes} from '../../components/withWindowDimensions'; import * as Localize from '../../libs/Localize'; +import * as StyleUtils from '../../styles/StyleUtils'; const propTypes = { /* Onyx Props */ @@ -145,7 +147,9 @@ class SignInPage extends Component { } return ( - + // There is an issue SafeAreaView on Android where wrong insets flicker on app start. + // Can be removed once https://github.com/th3rdwave/react-native-safe-area-context/issues/364 is resolved. + } {showUnlinkLoginForm && } - + ); } } @@ -171,6 +175,7 @@ SignInPage.propTypes = propTypes; SignInPage.defaultProps = defaultProps; export default compose( + withSafeAreaInsets, withLocalize, withWindowDimensions, withOnyx({ diff --git a/src/styles/StyleUtils.js b/src/styles/StyleUtils.js index a85c2d527f17..92ec8134c166 100644 --- a/src/styles/StyleUtils.js +++ b/src/styles/StyleUtils.js @@ -7,7 +7,6 @@ import colors from './colors'; import positioning from './utilities/positioning'; import styles from './styles'; import * as ReportUtils from '../libs/ReportUtils'; -import getSafeAreaPaddingTop from '../libs/getSafeAreaPaddingTop'; const workspaceColorOptions = [ {backgroundColor: colors.blue200, fill: colors.blue700}, @@ -172,15 +171,15 @@ function getDefaultWorkspaceAvatarColor(workspaceName) { * Takes safe area insets and returns padding to use for a View * * @param {Object} insets - * @param {Boolean} statusBarTranslucent + * @param {Number} [insetsPercentage] - Percentage of the insets to use for sides and bottom padding * @returns {Object} */ -function getSafeAreaPadding(insets, statusBarTranslucent) { +function getSafeAreaPadding(insets, insetsPercentage = variables.safeInsertPercentage) { return { - paddingTop: getSafeAreaPaddingTop(insets, statusBarTranslucent), - paddingBottom: insets.bottom * variables.safeInsertPercentage, - paddingLeft: insets.left * variables.safeInsertPercentage, - paddingRight: insets.right * variables.safeInsertPercentage, + paddingTop: insets.top, + paddingBottom: insets.bottom * insetsPercentage, + paddingLeft: insets.left * insetsPercentage, + paddingRight: insets.right * insetsPercentage, }; }