Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make status bar transparent on android #15778

Merged
merged 3 commits into from
May 25, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions android/app/src/main/java/com/expensify/chat/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
});
}

/**
Expand All @@ -54,26 +70,26 @@ 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);
return super.onKeyLongPress(keyCode, 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);
}
}
}
2 changes: 1 addition & 1 deletion android/app/src/main/res/values/styles.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<!-- Base application theme. Applied to all Android versions -->
<style name="BaseAppTheme" parent="Theme.AppCompat.NoActionBar">
<item name="android:colorEdgeEffect">@color/gray4</item>
<item name="android:statusBarColor">@color/status_bar_background</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="colorAccent">@color/accent</item>
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
<item name="popupTheme">@style/AppTheme.Popup</item>
Expand Down
20 changes: 6 additions & 14 deletions src/components/CustomStatusBar/index.android.js
Original file line number Diff line number Diff line change
@@ -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 <StatusBar />;
}
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;
}
8 changes: 3 additions & 5 deletions src/components/Modal/BaseModal.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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}
Julesssss marked this conversation as resolved.
Show resolved Hide resolved
deviceWidth={this.props.windowWidth}
animationIn={this.props.animationIn || animationIn}
animationOut={this.props.animationOut || animationOut}
Expand All @@ -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,
Expand Down
41 changes: 26 additions & 15 deletions src/components/withWindowDimensions.js
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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?
Expand All @@ -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,
};
}

Expand All @@ -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 <WindowDimensionsContext.Provider value={this.state}>{this.props.children}</WindowDimensionsContext.Provider>;
return (
<SafeAreaInsetsContext.Consumer>
{(insets) => {
const isSmallScreenWidth = this.state.windowWidth <= variables.mobileResponsiveWidthBreakpoint;
const isMediumScreenWidth = !isSmallScreenWidth && this.state.windowWidth <= variables.tabletResponsiveWidthBreakpoint;
const isLargeScreenWidth = !isSmallScreenWidth && !isMediumScreenWidth;
return (
<WindowDimensionsContext.Provider
value={{
windowHeight: this.state.windowHeight + getWindowHeightAdjustment(insets),
windowWidth: this.state.windowWidth,
isSmallScreenWidth,
isMediumScreenWidth,
isLargeScreenWidth,
}}
>
{this.props.children}
</WindowDimensionsContext.Provider>
);
}}
</SafeAreaInsetsContext.Consumer>
);
}
}

Expand Down
12 changes: 0 additions & 12 deletions src/libs/getSafeAreaPaddingTop/index.android.js

This file was deleted.

9 changes: 0 additions & 9 deletions src/libs/getSafeAreaPaddingTop/index.js

This file was deleted.

4 changes: 4 additions & 0 deletions src/libs/getWindowHeightAdjustment/index.android.js
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it better to use StatusBar.currentHeight directly?
Also I am thinking of deprecating getWindowHeightAdjustment platform files and put code directly here:

                            value={{
                                windowHeight: this.state.windowHeight + (StatusBar.currentHeight || 0),
                                windowWidth: this.state.windowWidth,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is preferable to use insets from react-native-safe-area-context instead of StatusBar.currentHeight which is a constant so it could have issues if we want to support screen rotation.

If we use insets then we do need platform files as it is implemented on all platform while StatusBar.currentHeight is android only.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file was needed only to calculate window height in android. No other usages.
Btw, not a problem.

}
4 changes: 4 additions & 0 deletions src/libs/getWindowHeightAdjustment/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Some platforms need to adjust the window height.
export default function getWindowHeightAdjustment() {
return 0;
}
11 changes: 8 additions & 3 deletions src/pages/signin/SignInPage.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 */
Expand Down Expand Up @@ -145,7 +147,9 @@ class SignInPage extends Component {
}

return (
<SafeAreaView style={[styles.signInPage]}>
// 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.
<View style={[styles.signInPage, StyleUtils.getSafeAreaPadding(this.props.insets, 1)]}>
janicduplessis marked this conversation as resolved.
Show resolved Hide resolved
<SignInPageLayout
welcomeHeader={welcomeHeader}
welcomeText={welcomeText}
Expand All @@ -162,7 +166,7 @@ class SignInPage extends Component {
{showResendValidationForm && <ResendValidationForm />}
{showUnlinkLoginForm && <UnlinkLoginForm />}
</SignInPageLayout>
</SafeAreaView>
</View>
);
}
}
Expand All @@ -171,6 +175,7 @@ SignInPage.propTypes = propTypes;
SignInPage.defaultProps = defaultProps;

export default compose(
withSafeAreaInsets,
withLocalize,
withWindowDimensions,
withOnyx({
Expand Down
13 changes: 6 additions & 7 deletions src/styles/StyleUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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,
};
}

Expand Down