Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -170,19 +170,6 @@ class ExponentManifest @Inject constructor(
const val MANIFEST_ANDROID_INFO_KEY = "android"
const val MANIFEST_KEYBOARD_LAYOUT_MODE_KEY = "softwareKeyboardLayoutMode"

// Statusbar
const val MANIFEST_STATUS_BAR_KEY = "androidStatusBar"
const val MANIFEST_STATUS_BAR_APPEARANCE = "barStyle"
const val MANIFEST_STATUS_BAR_BACKGROUND_COLOR = "backgroundColor"
const val MANIFEST_STATUS_BAR_HIDDEN = "hidden"
const val MANIFEST_STATUS_BAR_TRANSLUCENT = "translucent"

// NavigationBar
const val MANIFEST_NAVIGATION_BAR_KEY = "androidNavigationBar"
const val MANIFEST_NAVIGATION_BAR_VISIBILITY = "visible"
const val MANIFEST_NAVIGATION_BAR_APPEARANCE = "barStyle"
const val MANIFEST_NAVIGATION_BAR_BACKGROUND_COLOR = "backgroundColor"

// Debugging
const val MANIFEST_DEBUGGER_HOST_KEY = "debuggerHost"
const val MANIFEST_MAIN_MODULE_NAME_KEY = "mainModuleName"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,21 @@ import android.app.Activity
import android.content.pm.ActivityInfo
import android.view.WindowManager
import host.exp.exponent.ExponentManifest
import android.view.WindowInsets
import host.exp.exponent.ExponentManifest.BitmapListener
import android.graphics.Bitmap
import android.app.ActivityManager.TaskDescription
import android.graphics.Color
import android.os.Build
import android.view.View
import androidx.annotation.UiThread
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.view.ViewCompat
import expo.modules.jsonutils.getNullable
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import host.exp.exponent.analytics.EXL

object ExperienceActivityUtils {
private val TAG = ExperienceActivityUtils::class.java.simpleName

private const val STATUS_BAR_STYLE_DARK_CONTENT = "dark-content"
private const val STATUS_BAR_STYLE_LIGHT_CONTENT = "light-content"

fun updateOrientation(manifest: Manifest, activity: Activity) {
val orientation = manifest.getOrientation()
if (orientation == null) {
Expand Down Expand Up @@ -80,94 +75,29 @@ object ExperienceActivityUtils {

// region StatusBar configuration

/**
* React Native is not using flag [WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS] nor view/manifest attribute 'android:windowTranslucentStatus'
* (https://developer.android.com/reference/android/view/WindowManager.LayoutParams.html#FLAG_TRANSLUCENT_STATUS)
* (https://developer.android.com/reference/android/R.attr.html#windowTranslucentStatus)
* Instead it's using [WindowInsets] to limit available space on the screen ([com.facebook.react.modules.statusbar.StatusBarModule.setTranslucent]).
*
*
* In case 'android:'windowTranslucentStatus' is used in activity's theme, it has to be removed in order to make RN's Status Bar API work.
* Out approach to achieve translucency of StatusBar has to be aligned with RN's approach to ensure [com.facebook.react.modules.statusbar.StatusBarModule] works.
*
*
* Links to follow in case of need of more detailed understating.
* https://chris.banes.dev/talks/2017/becoming-a-master-window-fitter-lon/
* https://www.youtube.com/watch?v=_mGDMVRO3iE
*/
@Suppress("DEPRECATION")
fun configureStatusBar(manifest: Manifest, activity: Activity) {
val statusBarOptions = manifest.getAndroidStatusBarOptions()
val statusBarStyle = statusBarOptions?.getNullable<String>(ExponentManifest.MANIFEST_STATUS_BAR_APPEARANCE)
val statusBarOptions = manifest.getAndroidStatusBarOptions() ?: return

val statusBarHidden = statusBarOptions != null && statusBarOptions.optBoolean(
ExponentManifest.MANIFEST_STATUS_BAR_HIDDEN,
false
)
val style = statusBarOptions.optString("style")
val hidden = statusBarOptions.optBoolean("hidden")

activity.runOnUiThread {
// clear android:windowTranslucentStatus flag from Window as RN achieves translucency using WindowInsets
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)

setHidden(statusBarHidden, activity)

setTranslucent(activity)

setStyle(statusBarStyle, activity)

setColor(Color.TRANSPARENT, activity)
}
}

@UiThread
fun setColor(color: Int, activity: Activity) {
activity
.window
.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
activity
.window.statusBarColor = color
}
val window = activity.window

@UiThread
fun setTranslucent(activity: Activity) {
// As the status bar is translucent, hook into the window insets calculations
// and consume all the top insets so no padding will be added under the status bar.
val decorView = activity.window.decorView
decorView.setOnApplyWindowInsetsListener { v: View, insets: WindowInsets? ->
val defaultInsets = v.onApplyWindowInsets(insets)
defaultInsets.replaceSystemWindowInsets(
defaultInsets.systemWindowInsetLeft,
0,
defaultInsets.systemWindowInsetRight,
defaultInsets.systemWindowInsetBottom
)
}
ViewCompat.requestApplyInsets(decorView)
}
// clear android:windowTranslucentStatus flag as Window is edge-to-edge
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)

/**
* @return Effective style that is actually applied to the status bar.
*/
@UiThread
private fun setStyle(style: String?, activity: Activity) {
val decorView = activity.window.decorView
decorView.systemUiVisibility = when (style) {
STATUS_BAR_STYLE_LIGHT_CONTENT ->
decorView.systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
STATUS_BAR_STYLE_DARK_CONTENT ->
decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
else ->
decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}
}
WindowInsetsControllerCompat(window, window.decorView).run {
if (hidden) {
hide(WindowInsetsCompat.Type.statusBars())
}

@UiThread
private fun setHidden(hidden: Boolean, activity: Activity) {
if (hidden) {
activity.window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN)
} else {
activity.window.addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN)
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
when (style) {
"dark" -> isAppearanceLightStatusBars = true
"light" -> isAppearanceLightStatusBars = false
}
}
}
}

Expand Down Expand Up @@ -201,45 +131,34 @@ object ExperienceActivityUtils {
)
}

@Suppress("DEPRECATION")
fun setNavigationBar(manifest: Manifest, activity: Activity) {
val navBarOptions = manifest.getAndroidNavigationBarOptions() ?: return

// Set icon color of navigation bar
val navBarAppearance = navBarOptions.getNullable<String>(ExponentManifest.MANIFEST_NAVIGATION_BAR_APPEARANCE)
if (navBarAppearance != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try {
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
activity.window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
if (navBarAppearance == "dark-content") {
val decorView = activity.window.decorView
var flags = decorView.systemUiVisibility
flags = flags or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
decorView.systemUiVisibility = flags
}
} catch (e: Throwable) {
EXL.e(TAG, e)
val enforceContrast = navBarOptions.optBoolean("enforceContrast", true)
val style = navBarOptions.optString("style")
val hidden = navBarOptions.optBoolean("hidden")

activity.runOnUiThread {
val window = activity.window

// clear android:windowTranslucentNavigation flag as Window is edge-to-edge
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = enforceContrast
}
}

// Set visibility of navigation bar
val navBarVisible = navBarOptions.getNullable<String>(ExponentManifest.MANIFEST_NAVIGATION_BAR_VISIBILITY)
if (navBarVisible != null) {
// Hide both the navigation bar and the status bar. The Android docs recommend, "you should
// design your app to hide the status bar whenever you hide the navigation bar."
val decorView = activity.window.decorView
var flags = decorView.systemUiVisibility
when (navBarVisible) {
"leanback" ->
flags =
flags or (View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_FULLSCREEN)
"immersive" ->
flags =
flags or (View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_IMMERSIVE)
"sticky-immersive" ->
flags =
flags or (View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
WindowInsetsControllerCompat(window, window.decorView).run {
if (hidden) {
hide(WindowInsetsCompat.Type.navigationBars())
}

when (style) {
"dark" -> isAppearanceLightNavigationBars = true
"light" -> isAppearanceLightNavigationBars = false
}
}
decorView.systemUiVisibility = flags
}
}

Expand Down
3 changes: 0 additions & 3 deletions apps/native-component-list/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,6 @@
"web": {
"bundler": "metro"
},
"androidStatusBar": {
"backgroundColor": "#4630eb"
},
"newArchEnabled": true,
"android": {
"package": "host.exp.nclexp",
Expand Down
4 changes: 2 additions & 2 deletions docs/pages/router/advanced/drawer.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ A navigation drawer is a common pattern in mobile apps, it allows users to swipe
<Tabs>
<Tab label="SDK 54 and later">

To use [drawer navigator](https://reactnavigation.org/docs/drawer-based-navigation) you'll need to install some additional dependencies if you do not have them already. On Android and iOS, the drawer navigator requires `react-native-reanimated` and `react-native-worklets` to drive its animations. On web, this is handled by CSS animations.
To use [drawer navigator](https://reactnavigation.org/docs/drawer-navigator) you'll need to install some additional dependencies if you do not have them already. On Android and iOS, the drawer navigator requires `react-native-reanimated` and `react-native-worklets` to drive its animations. On web, this is handled by CSS animations.

<Terminal
cmd={[
Expand All @@ -30,7 +30,7 @@ To use [drawer navigator](https://reactnavigation.org/docs/drawer-based-navigati
</Tab>
<Tab label="SDK 53 and earlier">

To use [drawer navigator](https://reactnavigation.org/docs/drawer-based-navigation) you'll need to install some additional dependencies if you do not have them already. On Android and iOS, the drawer navigator requires `react-native-reanimated` and `react-native-gesture-handler` to drive its animations. On web, this is handled by CSS animations.
To use [drawer navigator](https://reactnavigation.org/docs/drawer-navigator) you'll need to install some additional dependencies if you do not have them already. On Android and iOS, the drawer navigator requires `react-native-reanimated` and `react-native-gesture-handler` to drive its animations. On web, this is handled by CSS animations.

<Terminal
cmd={[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,23 @@ const TEST_SCHEMA: Record<string, Property> = {
bareWorkflow: "Edit the 'Display Name' field in Xcode",
},
},
androidNavigationBar: {
description: 'Configuration for the bottom navigation bar on Android.',
splash: {
description: 'Configuration for loading and splash screen for standalone apps.',
type: 'object',
properties: {
visible: {
description: 'Determines how and when the navigation bar is shown.',
resizeMode: {
description: 'Determines how the image will be displayed in the splash loading screen.',
type: 'string',
properties: {
always: {
description: 'Test sub-sub-property',
type: 'boolean',
},
},
enum: ['leanback', 'immersive', 'sticky-immersive'],
enum: ['cover', 'contain'],
},
backgroundColor: {
description: 'Specifies the background color of the navigation bar. ',
description: 'Color to fill the loading screen background. ',
type: 'string',
pattern: '^#|(&#x23;)\\d{6}$',
meta: {
Expand Down Expand Up @@ -94,11 +94,9 @@ describe('AppConfigSchemaPropertiesTable', () => {
});

test('description includes all required components', () => {
renderWithHeadings(
<AppConfigSchemaTable schema={{ entry: TEST_SCHEMA.androidNavigationBar }} />
);
renderWithHeadings(<AppConfigSchemaTable schema={{ entry: TEST_SCHEMA.splash }} />);

expect(screen.getByText('Specifies the background color of the navigation bar.'));
expect(screen.getByText('Color to fill the loading screen background.'));
expect(screen.getByText('6 character long hex color string, eg:'));
});
});
Expand All @@ -108,16 +106,16 @@ describe('formatSchema', () => {
test('name is property at root level', () => {
expect(formattedSchema[0].name).toBe('name');
});
test('androidNavigationBar has two subproperties', () => {
test('splash has two subproperties', () => {
expect(formattedSchema[1].subproperties.length).toBe(2);
});
test('visible is androidNavigationBar subproperty', () => {
expect(formattedSchema[1].subproperties[0].name).toBe('visible');
test('resizeMode is splash subproperty', () => {
expect(formattedSchema[1].subproperties[0].name).toBe('resizeMode');
});
test('always is visible subproperty', () => {
test('always is resizeMode subproperty', () => {
expect(formattedSchema[1].subproperties[0].subproperties[0].name).toBe('always');
});
test('backgroundColor is androidNavigationBar subproperty', () => {
test('backgroundColor is splash subproperty', () => {
expect(formattedSchema[1].subproperties[1].name).toBe('backgroundColor');
});
test('intentFilters is property at root level', () => {
Expand Down
Loading
Loading