Properly handle android system bars (#9242)
This commit is contained in:
parent
5d47f062f3
commit
c0eaeefbe0
6 changed files with 136 additions and 14 deletions
|
|
@ -3,6 +3,7 @@ package com.mattermost.rnbeta
|
|||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import androidx.core.view.WindowCompat
|
||||
import com.facebook.react.ReactActivityDelegate
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate
|
||||
|
|
@ -37,6 +38,7 @@ class MainActivity : NavigationActivity() {
|
|||
setHWKeyboardConnected()
|
||||
lastOrientation = this.resources.configuration.orientation
|
||||
foldableObserver.onCreate()
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
|
|
|||
|
|
@ -7,5 +7,14 @@
|
|||
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
|
||||
<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
|
||||
|
||||
|
||||
<item name="android:navigationBarDividerColor">@android:color/transparent</item>
|
||||
|
||||
<!-- Avoid forced contrast paddings on some OEMs -->
|
||||
<item name="android:enforceStatusBarContrast">false</item>
|
||||
<item name="android:enforceNavigationBarContrast">false</item>
|
||||
|
||||
<!-- Support cutouts -->
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@
|
|||
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
import RNUtils from '@mattermost/rnutils';
|
||||
import merge from 'deepmerge';
|
||||
import {Appearance, DeviceEventEmitter, StatusBar, Platform, Alert, type EmitterSubscription, Keyboard} from 'react-native';
|
||||
import {Appearance, DeviceEventEmitter, Platform, Alert, type EmitterSubscription, Keyboard, StatusBar} from 'react-native';
|
||||
import {type ComponentWillAppearEvent, type ImageResource, type LayoutOrientation, Navigation, type Options, OptionsModalPresentationStyle, type OptionsTopBarButton, type ScreenPoppedEvent, type EventSubscription} from 'react-native-navigation';
|
||||
import tinyColor from 'tinycolor2';
|
||||
|
||||
|
|
@ -36,6 +37,23 @@ let subscriptions: Array<EmitterSubscription | EventSubscription> | undefined;
|
|||
export const allOrientations: LayoutOrientation[] = ['sensor', 'sensorLandscape', 'sensorPortrait', 'landscape', 'portrait'];
|
||||
export const portraitOrientation: LayoutOrientation[] = ['portrait'];
|
||||
|
||||
const loginFlowScreens = new Set<AvailableScreens>([
|
||||
Screens.ONBOARDING,
|
||||
Screens.SERVER,
|
||||
Screens.LOGIN,
|
||||
Screens.SSO,
|
||||
Screens.MFA,
|
||||
Screens.FORGOT_PASSWORD,
|
||||
]);
|
||||
|
||||
function setNavigationBarColor(screen: AvailableScreens, th?: Theme) {
|
||||
if (Platform.OS === 'android' && Platform.Version >= 34) {
|
||||
const theme = th || getThemeFromState();
|
||||
const color = loginFlowScreens.has(screen) ? theme.sidebarBg : theme.centerChannelBg;
|
||||
RNUtils.setNavigationBarColor(color, tinyColor(color).isLight());
|
||||
}
|
||||
}
|
||||
|
||||
export function registerNavigationListeners() {
|
||||
subscriptions?.forEach((v) => v.remove());
|
||||
subscriptions = [
|
||||
|
|
@ -88,14 +106,19 @@ function onCommandListener(name: string, params: any) {
|
|||
}
|
||||
}
|
||||
|
||||
if (NavigationStore.getVisibleScreen() === Screens.HOME) {
|
||||
const screen = NavigationStore.getVisibleScreen();
|
||||
if (screen === Screens.HOME) {
|
||||
DeviceEventEmitter.emit(Events.TAB_BAR_VISIBLE, true);
|
||||
}
|
||||
|
||||
setNavigationBarColor(screen);
|
||||
}
|
||||
|
||||
function onPoppedListener({componentId}: ScreenPoppedEvent) {
|
||||
// screen pop does not trigger registerCommandListener, but does trigger screenPoppedListener
|
||||
NavigationStore.removeScreenFromStack(componentId as AvailableScreens);
|
||||
const screen = componentId as AvailableScreens;
|
||||
NavigationStore.removeScreenFromStack(screen);
|
||||
setNavigationBarColor(screen);
|
||||
}
|
||||
|
||||
function onScreenWillAppear(event: ComponentWillAppearEvent) {
|
||||
|
|
@ -279,6 +302,32 @@ function isScreenRegistered(screen: AvailableScreens) {
|
|||
return true;
|
||||
}
|
||||
|
||||
function edgeToEdgeHack(screen: AvailableScreens, theme: Theme) {
|
||||
const isDark = tinyColor(theme.sidebarBg).isDark();
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
if (Platform.Version >= 34) {
|
||||
const listener = Navigation.events().registerComponentDidAppearListener((event) => {
|
||||
if (event.componentName === screen) {
|
||||
setNavigationBarColor(screen, theme);
|
||||
listener.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (Platform.Version >= 36) {
|
||||
return {
|
||||
drawBehind: true,
|
||||
translucent: false,
|
||||
isDark,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
StatusBar.setBarStyle(isDark ? 'light-content' : 'dark-content');
|
||||
return {isDark};
|
||||
}
|
||||
|
||||
export function openToS() {
|
||||
NavigationStore.setToSOpen(true);
|
||||
return showOverlay(Screens.TERMS_OF_SERVICE, {}, {overlay: {interceptTouchOutside: true}});
|
||||
|
|
@ -286,8 +335,7 @@ export function openToS() {
|
|||
|
||||
export function resetToHome(passProps: LaunchProps = {launchType: Launch.Normal}) {
|
||||
const theme = getThemeFromState();
|
||||
const isDark = tinyColor(theme.sidebarBg).isDark();
|
||||
StatusBar.setBarStyle(isDark ? 'light-content' : 'dark-content');
|
||||
const edgeToEdge = edgeToEdgeHack(Screens.HOME, theme);
|
||||
|
||||
if (!passProps.coldStart && (passProps.launchType === Launch.AddServer || passProps.launchType === Launch.AddServerFromDeepLink)) {
|
||||
dismissModal({componentId: Screens.SERVER});
|
||||
|
|
@ -313,6 +361,7 @@ export function resetToHome(passProps: LaunchProps = {launchType: Launch.Normal}
|
|||
statusBar: {
|
||||
visible: true,
|
||||
backgroundColor: theme.sidebarBg,
|
||||
...edgeToEdge,
|
||||
},
|
||||
topBar: {
|
||||
visible: false,
|
||||
|
|
@ -337,8 +386,7 @@ export function resetToHome(passProps: LaunchProps = {launchType: Launch.Normal}
|
|||
|
||||
export function resetToSelectServer(passProps: LaunchProps) {
|
||||
const theme = getDefaultThemeByAppearance();
|
||||
const isDark = tinyColor(theme.sidebarBg).isDark();
|
||||
StatusBar.setBarStyle(isDark ? 'light-content' : 'dark-content');
|
||||
const edgeToEdge = edgeToEdgeHack(Screens.SERVER, theme);
|
||||
|
||||
const children = [{
|
||||
component: {
|
||||
|
|
@ -356,6 +404,7 @@ export function resetToSelectServer(passProps: LaunchProps) {
|
|||
statusBar: {
|
||||
visible: true,
|
||||
backgroundColor: theme.sidebarBg,
|
||||
...edgeToEdge,
|
||||
},
|
||||
topBar: {
|
||||
backButton: {
|
||||
|
|
@ -383,8 +432,7 @@ export function resetToSelectServer(passProps: LaunchProps) {
|
|||
|
||||
export function resetToOnboarding(passProps: LaunchProps) {
|
||||
const theme = getDefaultThemeByAppearance();
|
||||
const isDark = tinyColor(theme.sidebarBg).isDark();
|
||||
StatusBar.setBarStyle(isDark ? 'light-content' : 'dark-content');
|
||||
const edgeToEdge = edgeToEdgeHack(Screens.ONBOARDING, theme);
|
||||
|
||||
const children = [{
|
||||
component: {
|
||||
|
|
@ -402,6 +450,7 @@ export function resetToOnboarding(passProps: LaunchProps) {
|
|||
statusBar: {
|
||||
visible: true,
|
||||
backgroundColor: theme.sidebarBg,
|
||||
...edgeToEdge,
|
||||
},
|
||||
topBar: {
|
||||
backButton: {
|
||||
|
|
@ -429,8 +478,7 @@ export function resetToOnboarding(passProps: LaunchProps) {
|
|||
|
||||
export function resetToTeams() {
|
||||
const theme = getThemeFromState();
|
||||
const isDark = tinyColor(theme.sidebarBg).isDark();
|
||||
StatusBar.setBarStyle(isDark ? 'light-content' : 'dark-content');
|
||||
const edgeToEdge = edgeToEdgeHack(Screens.SELECT_TEAM, theme);
|
||||
|
||||
return Navigation.setRoot({
|
||||
root: {
|
||||
|
|
@ -446,6 +494,7 @@ export function resetToTeams() {
|
|||
statusBar: {
|
||||
visible: true,
|
||||
backgroundColor: theme.sidebarBg,
|
||||
...edgeToEdge,
|
||||
},
|
||||
topBar: {
|
||||
visible: false,
|
||||
|
|
@ -472,7 +521,7 @@ export function goToScreen(name: AvailableScreens, title: string, passProps = {}
|
|||
}
|
||||
|
||||
const theme = getThemeFromState();
|
||||
const isDark = tinyColor(theme.sidebarBg).isDark();
|
||||
const edgeToEdge = edgeToEdgeHack(name, theme);
|
||||
const componentId = NavigationStore.getVisibleScreen();
|
||||
if (!componentId) {
|
||||
logError('Trying to go to screen without any screen on the navigation store');
|
||||
|
|
@ -489,8 +538,10 @@ export function goToScreen(name: AvailableScreens, title: string, passProps = {}
|
|||
right: {enabled: false},
|
||||
},
|
||||
statusBar: {
|
||||
style: isDark ? 'light' : 'dark',
|
||||
style: edgeToEdge.isDark ? 'light' : 'dark',
|
||||
backgroundColor: theme.sidebarBg,
|
||||
drawBehind: edgeToEdge.drawBehind ?? false,
|
||||
translucent: edgeToEdge.translucent ?? false,
|
||||
},
|
||||
topBar: {
|
||||
animate: true,
|
||||
|
|
@ -608,6 +659,7 @@ export function showModal(name: AvailableScreens, title: string, passProps = {},
|
|||
}
|
||||
|
||||
const theme = getThemeFromState();
|
||||
const edgeToEdge = edgeToEdgeHack(name, theme);
|
||||
const modalPresentationStyle: OptionsModalPresentationStyle = Platform.OS === 'ios' ? OptionsModalPresentationStyle.pageSheet : OptionsModalPresentationStyle.none;
|
||||
const defaultOptions: Options = {
|
||||
modalPresentationStyle,
|
||||
|
|
@ -617,6 +669,7 @@ export function showModal(name: AvailableScreens, title: string, passProps = {},
|
|||
statusBar: {
|
||||
visible: true,
|
||||
backgroundColor: theme.sidebarBg,
|
||||
...edgeToEdge,
|
||||
},
|
||||
topBar: {
|
||||
animate: true,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
package com.mattermost.rnutils
|
||||
|
||||
import android.app.Activity
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.view.WindowManager
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.Promise
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
|
|
@ -13,8 +16,13 @@ import com.mattermost.rnutils.helpers.Notifications
|
|||
import com.mattermost.rnutils.helpers.RealPathUtil
|
||||
import com.mattermost.rnutils.helpers.SaveDataTask
|
||||
import com.mattermost.rnutils.helpers.SplitView
|
||||
import androidx.core.graphics.toColorInt
|
||||
import com.facebook.react.bridge.LifecycleEventListener
|
||||
|
||||
class RNUtilsModuleImpl(private val reactContext: ReactApplicationContext) {
|
||||
class RNUtilsModuleImpl(private val reactContext: ReactApplicationContext): LifecycleEventListener {
|
||||
|
||||
private var lastHex: String? = null
|
||||
private var lastLightIcons: Boolean = true
|
||||
|
||||
companion object {
|
||||
const val NAME = "RNUtils"
|
||||
|
|
@ -37,8 +45,17 @@ class RNUtilsModuleImpl(private val reactContext: ReactApplicationContext) {
|
|||
setCtx(reactContext)
|
||||
SplitView.setCtx(reactContext)
|
||||
Notifications.setCtx(reactContext)
|
||||
reactContext.addLifecycleEventListener(this)
|
||||
}
|
||||
|
||||
override fun onHostResume() {
|
||||
val hex = lastHex ?: return
|
||||
val light = lastLightIcons
|
||||
setNavigationBarColor(hex, light)
|
||||
}
|
||||
override fun onHostPause() {}
|
||||
override fun onHostDestroy() {}
|
||||
|
||||
fun getTypedExportedConstants(): MutableMap<String, Any> {
|
||||
val map = mutableMapOf<String, Any>()
|
||||
val group = mutableMapOf<String, Any>()
|
||||
|
|
@ -213,4 +230,36 @@ class RNUtilsModuleImpl(private val reactContext: ReactApplicationContext) {
|
|||
currentActivity.window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
|
||||
}
|
||||
}
|
||||
|
||||
fun setNavigationBarColor(colorHex: String, lightIcons: Boolean) {
|
||||
val currentActivity: Activity = reactContext.currentActivity ?: return
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
return
|
||||
}
|
||||
|
||||
lastHex = colorHex
|
||||
lastLightIcons = lightIcons
|
||||
|
||||
currentActivity.runOnUiThread {
|
||||
try {
|
||||
val w = currentActivity.window
|
||||
w.decorView.post {
|
||||
val parsedColor = colorHex.toColorInt()
|
||||
w.navigationBarColor = parsedColor
|
||||
|
||||
val controller = WindowInsetsControllerCompat(w, w.decorView)
|
||||
controller.isAppearanceLightNavigationBars = lightIcons
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
w.isNavigationBarContrastEnforced = false
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
w.navigationBarDividerColor = Color.TRANSPARENT
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("RNUtils", "Error setting navigation bar color: $colorHex", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.mattermost.rnutils
|
||||
|
||||
import com.facebook.react.bridge.LifecycleEventListener
|
||||
import com.facebook.react.bridge.Promise
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
||||
|
|
@ -115,4 +116,9 @@ class RNUtilsModule(context: ReactApplicationContext) :
|
|||
val pathList = paths.toArrayList().map { it.toString() }
|
||||
implementation.createZipFile(pathList, promise)
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun setNavigationBarColor(colorHex: String, lightIcons: Boolean) {
|
||||
implementation.setNavigationBarColor(colorHex, lightIcons)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,9 @@ export interface Spec extends TurboModule {
|
|||
setSoftKeyboardToAdjustNothing(): void;
|
||||
|
||||
createZipFile: (paths: string[]) => Promise<string>;
|
||||
|
||||
// Android only
|
||||
setNavigationBarColor: (color: string, lightIcons: boolean) => void;
|
||||
}
|
||||
|
||||
export default TurboModuleRegistry.getEnforcing<Spec>('RNUtils');
|
||||
|
|
|
|||
Loading…
Reference in a new issue