Skip to content
Open
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 @@ -15,6 +15,7 @@
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Window;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.common.logging.FLog;
Expand Down Expand Up @@ -48,6 +49,8 @@ public interface RCTDeviceEventEmitter extends JavaScriptModule {
new CopyOnWriteArraySet<>();
private final CopyOnWriteArraySet<ActivityEventListener> mActivityEventListeners =
new CopyOnWriteArraySet<>();
private final CopyOnWriteArraySet<WindowEventListener> mWindowEventListeners =
new CopyOnWriteArraySet<>();
private final CopyOnWriteArraySet<WindowFocusChangeListener> mWindowFocusEventListeners =
new CopyOnWriteArraySet<>();
private final ScrollEndedListeners mScrollEndedListeners = new ScrollEndedListeners();
Expand Down Expand Up @@ -246,6 +249,14 @@ public void removeActivityEventListener(ActivityEventListener listener) {
mActivityEventListeners.remove(listener);
}

public void addWindowEventListener(WindowEventListener listener) {
mWindowEventListeners.add(listener);
}

public void removeWindowEventListener(WindowEventListener listener) {
mWindowEventListeners.remove(listener);
}

public void addWindowFocusChangeListener(WindowFocusChangeListener listener) {
mWindowFocusEventListeners.add(listener);
}
Expand Down Expand Up @@ -356,6 +367,30 @@ public void onActivityResult(
}
}

@ThreadConfined(UI)
public void onWindowCreated(Window window) {
UiThreadUtil.assertOnUiThread();
for (WindowEventListener listener : mWindowEventListeners) {
try {
listener.onWindowCreated(window);
} catch (RuntimeException e) {
handleException(e);
}
}
}

@ThreadConfined(UI)
public void onWindowDestroyed(Window window) {
UiThreadUtil.assertOnUiThread();
for (WindowEventListener listener : mWindowEventListeners) {
try {
listener.onWindowDestroyed(window);
} catch (RuntimeException e) {
handleException(e);
}
}
}

@ThreadConfined(UI)
public void onWindowFocusChange(boolean hasFocus) {
UiThreadUtil.assertOnUiThread();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

package com.facebook.react.bridge

import android.view.Window

/**
* Listener for receiving window creation and destruction events.
*
* This allows modules to react to new windows being added or removed, such as Dialog windows
* created by Modal components. Modules like StatusBarModule can implement this interface to apply
* their configuration to all active windows.
*
* Third-party libraries can both implement this listener and emit window events through
* [ReactContext.onWindowCreated] and [ReactContext.onWindowDestroyed].
*/
public interface WindowEventListener {

/** Called when a new [Window] is created (e.g. a Dialog window for a Modal). */
public fun onWindowCreated(window: Window)

/** Called when a [Window] is about to be destroyed (e.g. a Dialog window being dismissed). */
public fun onWindowDestroyed(window: Window)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,59 @@

package com.facebook.react.modules.statusbar

import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
import android.os.Build
import android.view.View
import android.view.WindowInsetsController
import android.view.WindowManager
import android.view.Window
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import com.facebook.common.logging.FLog
import com.facebook.fbreact.specs.NativeStatusBarManagerAndroidSpec
import com.facebook.react.bridge.GuardedRunnable
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.bridge.WindowEventListener
import com.facebook.react.common.ReactConstants
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.DisplayMetricsHolder.getStatusBarHeightPx
import com.facebook.react.uimanager.PixelUtil
import com.facebook.react.views.view.isEdgeToEdgeFeatureFlagOn
import com.facebook.react.views.view.setStatusBarColor
import com.facebook.react.views.view.setStatusBarStyle
import com.facebook.react.views.view.setStatusBarTranslucency
import com.facebook.react.views.view.setStatusBarVisibility

/** [NativeModule] that allows changing the appearance of the status bar. */
@ReactModule(name = NativeStatusBarManagerAndroidSpec.NAME)
internal class StatusBarModule(reactContext: ReactApplicationContext?) :
NativeStatusBarManagerAndroidSpec(reactContext) {
NativeStatusBarManagerAndroidSpec(reactContext), WindowEventListener {

private val extrasWindows = mutableSetOf<Window>()

init {
reactApplicationContext.addWindowEventListener(this)
}

override fun invalidate() {
super.invalidate()
reactApplicationContext.removeWindowEventListener(this)
}

override fun onWindowCreated(window: Window) {
extrasWindows.add(window)

UiThreadUtil.runOnUiThread {
val controller = WindowCompat.getInsetsController(window, window.decorView)
val insets = ViewCompat.getRootWindowInsets(window.decorView)
val style = if (controller.isAppearanceLightStatusBars) "dark-content" else "light-content"
val visible = insets?.isVisible(WindowInsetsCompat.Type.statusBars()) ?: true

window.setStatusBarStyle(style)
window.setStatusBarVisibility(!visible)
}
}

override fun onWindowDestroyed(window: Window) {
extrasWindows.remove(window)
}

@Suppress("DEPRECATION")
override fun getTypedExportedConstants(): Map<String, Any> {
Expand All @@ -45,7 +74,6 @@ internal class StatusBarModule(reactContext: ReactApplicationContext?) :
)
}

@Suppress("DEPRECATION")
override fun setColor(colorDouble: Double, animated: Boolean) {
val color = colorDouble.toInt()
val activity = reactApplicationContext.getCurrentActivity()
Expand All @@ -63,25 +91,7 @@ internal class StatusBarModule(reactContext: ReactApplicationContext?) :
)
return
}
UiThreadUtil.runOnUiThread(
object : GuardedRunnable(reactApplicationContext) {
override fun runGuarded() {
val window = activity.window ?: return
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
if (animated) {
val curColor = window.statusBarColor
val colorAnimation = ValueAnimator.ofObject(ArgbEvaluator(), curColor, color)
colorAnimation.addUpdateListener { animator ->
activity.window?.statusBarColor = (animator.animatedValue as Int)
}
colorAnimation.setDuration(300).startDelay = 0
colorAnimation.start()
} else {
window.statusBarColor = color
}
}
}
)
UiThreadUtil.runOnUiThread { activity.window?.setStatusBarColor(color, animated) }
}

override fun setTranslucent(translucent: Boolean) {
Expand All @@ -100,13 +110,7 @@ internal class StatusBarModule(reactContext: ReactApplicationContext?) :
)
return
}
UiThreadUtil.runOnUiThread(
object : GuardedRunnable(reactApplicationContext) {
override fun runGuarded() {
activity.window?.setStatusBarTranslucency(translucent)
}
}
)
UiThreadUtil.runOnUiThread { activity.window?.setStatusBarTranslucency(translucent) }
}

override fun setHidden(hidden: Boolean) {
Expand All @@ -118,10 +122,12 @@ internal class StatusBarModule(reactContext: ReactApplicationContext?) :
)
return
}
UiThreadUtil.runOnUiThread { activity.window?.setStatusBarVisibility(hidden) }
UiThreadUtil.runOnUiThread {
activity.window?.setStatusBarVisibility(hidden)
extrasWindows.forEach { it.setStatusBarVisibility(hidden) }
}
}

@Suppress("DEPRECATION")
override fun setStyle(style: String?) {
val activity = reactApplicationContext.getCurrentActivity()
if (activity == null) {
Expand All @@ -131,36 +137,10 @@ internal class StatusBarModule(reactContext: ReactApplicationContext?) :
)
return
}
UiThreadUtil.runOnUiThread(
Runnable {
val window = activity.window ?: return@Runnable
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
val insetsController = window.insetsController ?: return@Runnable
if ("dark-content" == style) {
// dark-content means dark icons on a light status bar
insetsController.setSystemBarsAppearance(
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS,
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS,
)
} else {
insetsController.setSystemBarsAppearance(
0,
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS,
)
}
} else {
val decorView = window.decorView
var systemUiVisibilityFlags = decorView.systemUiVisibility
systemUiVisibilityFlags =
if ("dark-content" == style) {
systemUiVisibilityFlags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
} else {
systemUiVisibilityFlags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
}
decorView.systemUiVisibility = systemUiVisibilityFlags
}
}
)
UiThreadUtil.runOnUiThread {
activity.window?.setStatusBarStyle(style)
extrasWindows.forEach { it.setStatusBarStyle(style) }
}
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ package com.facebook.react.uimanager

import android.app.Activity
import android.content.Context
import android.view.Window
import com.facebook.react.bridge.Callback
import com.facebook.react.bridge.CatalystInstance
import com.facebook.react.bridge.JavaScriptContextHolder
Expand All @@ -20,6 +21,7 @@ import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.ScrollEndedListeners
import com.facebook.react.bridge.WindowEventListener
import com.facebook.react.bridge.UIManager
import com.facebook.react.common.annotations.internal.LegacyArchitecture
import com.facebook.react.turbomodule.core.interfaces.CallInvokerHolder
Expand Down Expand Up @@ -67,6 +69,22 @@ public class ThemedReactContext(
reactApplicationContext.removeLifecycleEventListener(listener)
}

override fun addWindowEventListener(listener: WindowEventListener) {
reactApplicationContext.addWindowEventListener(listener)
}

override fun removeWindowEventListener(listener: WindowEventListener) {
reactApplicationContext.removeWindowEventListener(listener)
}

override fun onWindowCreated(window: Window) {
reactApplicationContext.onWindowCreated(window)
}

override fun onWindowDestroyed(window: Window) {
reactApplicationContext.onWindowDestroyed(window)
}

override fun hasCurrentActivity(): Boolean = reactApplicationContext.hasCurrentActivity()

override fun getCurrentActivity(): Activity? = reactApplicationContext.getCurrentActivity()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,9 @@ public class ReactModalHostView(context: ThemedReactContext) :
UiThreadUtil.assertOnUiThread()

dialog?.let { nonNullDialog ->
nonNullDialog.window?.let { window ->
(context as ThemedReactContext).onWindowDestroyed(window)
}
if (nonNullDialog.isShowing) {
val dialogContext =
ContextUtils.findContextOfType(nonNullDialog.context, Activity::class.java)
Expand Down Expand Up @@ -341,6 +344,7 @@ public class ReactModalHostView(context: ThemedReactContext) :
newDialog.show()
updateSystemAppearance()
window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
(context as ThemedReactContext).onWindowCreated(window)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@

package com.facebook.react.views.view

import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
import android.graphics.Color
import android.os.Build
import android.view.View
import android.view.Window
import android.view.WindowInsetsController
import android.view.WindowManager
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
Expand Down Expand Up @@ -37,6 +41,49 @@ public fun setEdgeToEdgeFeatureFlagOn() {
isEdgeToEdgeFeatureFlagOn = true
}

@Suppress("DEPRECATION")
internal fun Window.setStatusBarColor(color: Int, animated: Boolean) {
addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
if (animated) {
val curColor = statusBarColor
val colorAnimation = ValueAnimator.ofObject(ArgbEvaluator(), curColor, color)
colorAnimation.addUpdateListener { animator ->
statusBarColor = (animator.animatedValue as Int)
}
colorAnimation.setDuration(300).startDelay = 0
colorAnimation.start()
} else {
statusBarColor = color
}
}

@Suppress("DEPRECATION")
internal fun Window.setStatusBarStyle(style: String?) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
if ("dark-content" == style) {
// dark-content means dark icons on a light status bar
insetsController?.setSystemBarsAppearance(
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS,
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS,
)
} else {
insetsController?.setSystemBarsAppearance(
0,
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS,
)
}
} else {
var systemUiVisibilityFlags = decorView.systemUiVisibility
systemUiVisibilityFlags =
if ("dark-content" == style) {
systemUiVisibilityFlags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
} else {
systemUiVisibilityFlags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
}
decorView.systemUiVisibility = systemUiVisibilityFlags
}
}

@Suppress("DEPRECATION")
internal fun Window.setStatusBarTranslucency(isTranslucent: Boolean) {
// If the status bar is translucent hook into the window insets calculations
Expand Down
Loading