Skip to content

Commit ad6d282

Browse files
committed
Fix StatusBar not applying to Modal windows
1 parent 41a1941 commit ad6d282

6 files changed

Lines changed: 180 additions & 67 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.bridge
9+
10+
import android.view.Window
11+
12+
/**
13+
* Listener for receiving extra window creation and destruction events.
14+
*
15+
* This allows modules to react to new windows being added or removed, such as Dialog windows
16+
* registered by Modal components. Modules like StatusBarModule can implement this interface to
17+
* apply their configuration to all active windows.
18+
*
19+
* Third-party libraries can both implement this listener and emit window events through
20+
* [ReactContext.onExtraWindowCreate] and [ReactContext.onExtraWindowDestroy].
21+
*/
22+
public interface ExtraWindowEventListener {
23+
24+
/** Called when a new [Window] is created (e.g. a Dialog window for a Modal). */
25+
public fun onExtraWindowCreate(window: Window)
26+
27+
/** Called when a [Window] is destroyed (e.g. on Dialog window dismiss). */
28+
public fun onExtraWindowDestroy(window: Window)
29+
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContext.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import android.content.Intent;
1616
import android.os.Bundle;
1717
import android.view.LayoutInflater;
18+
import android.view.Window;
1819
import androidx.annotation.NonNull;
1920
import androidx.annotation.Nullable;
2021
import com.facebook.common.logging.FLog;
@@ -48,6 +49,8 @@ public interface RCTDeviceEventEmitter extends JavaScriptModule {
4849
new CopyOnWriteArraySet<>();
4950
private final CopyOnWriteArraySet<ActivityEventListener> mActivityEventListeners =
5051
new CopyOnWriteArraySet<>();
52+
private final CopyOnWriteArraySet<ExtraWindowEventListener> mExtraWindowEventListeners =
53+
new CopyOnWriteArraySet<>();
5154
private final CopyOnWriteArraySet<WindowFocusChangeListener> mWindowFocusEventListeners =
5255
new CopyOnWriteArraySet<>();
5356
private final ScrollEndedListeners mScrollEndedListeners = new ScrollEndedListeners();
@@ -246,6 +249,14 @@ public void removeActivityEventListener(ActivityEventListener listener) {
246249
mActivityEventListeners.remove(listener);
247250
}
248251

252+
public void addExtraWindowEventListener(ExtraWindowEventListener listener) {
253+
mExtraWindowEventListeners.add(listener);
254+
}
255+
256+
public void removeExtraWindowEventListener(ExtraWindowEventListener listener) {
257+
mExtraWindowEventListeners.remove(listener);
258+
}
259+
249260
public void addWindowFocusChangeListener(WindowFocusChangeListener listener) {
250261
mWindowFocusEventListeners.add(listener);
251262
}
@@ -356,6 +367,30 @@ public void onActivityResult(
356367
}
357368
}
358369

370+
@ThreadConfined(UI)
371+
public void onExtraWindowCreate(Window window) {
372+
UiThreadUtil.assertOnUiThread();
373+
for (ExtraWindowEventListener listener : mExtraWindowEventListeners) {
374+
try {
375+
listener.onExtraWindowCreate(window);
376+
} catch (RuntimeException e) {
377+
handleException(e);
378+
}
379+
}
380+
}
381+
382+
@ThreadConfined(UI)
383+
public void onExtraWindowDestroy(Window window) {
384+
UiThreadUtil.assertOnUiThread();
385+
for (ExtraWindowEventListener listener : mExtraWindowEventListeners) {
386+
try {
387+
listener.onExtraWindowDestroy(window);
388+
} catch (RuntimeException e) {
389+
handleException(e);
390+
}
391+
}
392+
}
393+
359394
@ThreadConfined(UI)
360395
public void onWindowFocusChange(boolean hasFocus) {
361396
UiThreadUtil.assertOnUiThread();

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/statusbar/StatusBarModule.kt

Lines changed: 47 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,13 @@
77

88
package com.facebook.react.modules.statusbar
99

10-
import android.animation.ArgbEvaluator
11-
import android.animation.ValueAnimator
12-
import android.os.Build
13-
import android.view.View
14-
import android.view.WindowInsetsController
15-
import android.view.WindowManager
10+
import android.view.Window
11+
import androidx.core.view.ViewCompat
12+
import androidx.core.view.WindowCompat
13+
import androidx.core.view.WindowInsetsCompat
1614
import com.facebook.common.logging.FLog
1715
import com.facebook.fbreact.specs.NativeStatusBarManagerAndroidSpec
18-
import com.facebook.react.bridge.GuardedRunnable
16+
import com.facebook.react.bridge.ExtraWindowEventListener
1917
import com.facebook.react.bridge.NativeModule
2018
import com.facebook.react.bridge.ReactApplicationContext
2119
import com.facebook.react.bridge.UiThreadUtil
@@ -24,13 +22,44 @@ import com.facebook.react.module.annotations.ReactModule
2422
import com.facebook.react.uimanager.DisplayMetricsHolder.getStatusBarHeightPx
2523
import com.facebook.react.uimanager.PixelUtil
2624
import com.facebook.react.views.view.isEdgeToEdgeFeatureFlagOn
25+
import com.facebook.react.views.view.setStatusBarColor
26+
import com.facebook.react.views.view.setStatusBarStyle
2727
import com.facebook.react.views.view.setStatusBarTranslucency
2828
import com.facebook.react.views.view.setStatusBarVisibility
2929

3030
/** [NativeModule] that allows changing the appearance of the status bar. */
3131
@ReactModule(name = NativeStatusBarManagerAndroidSpec.NAME)
3232
internal class StatusBarModule(reactContext: ReactApplicationContext?) :
33-
NativeStatusBarManagerAndroidSpec(reactContext) {
33+
NativeStatusBarManagerAndroidSpec(reactContext), ExtraWindowEventListener {
34+
35+
private val extraWindows = mutableSetOf<Window>()
36+
37+
init {
38+
reactApplicationContext.addExtraWindowEventListener(this)
39+
}
40+
41+
override fun invalidate() {
42+
super.invalidate()
43+
reactApplicationContext.removeExtraWindowEventListener(this)
44+
}
45+
46+
override fun onExtraWindowCreate(window: Window) {
47+
extraWindows.add(window)
48+
49+
UiThreadUtil.runOnUiThread {
50+
val controller = WindowCompat.getInsetsController(window, window.decorView)
51+
val insets = ViewCompat.getRootWindowInsets(window.decorView)
52+
val style = if (controller.isAppearanceLightStatusBars) "dark-content" else "light-content"
53+
val visible = insets?.isVisible(WindowInsetsCompat.Type.statusBars()) ?: true
54+
55+
window.setStatusBarStyle(style)
56+
window.setStatusBarVisibility(!visible)
57+
}
58+
}
59+
60+
override fun onExtraWindowDestroy(window: Window) {
61+
extraWindows.remove(window)
62+
}
3463

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

48-
@Suppress("DEPRECATION")
4977
override fun setColor(colorDouble: Double, animated: Boolean) {
5078
val color = colorDouble.toInt()
5179
val activity = reactApplicationContext.getCurrentActivity()
@@ -63,25 +91,7 @@ internal class StatusBarModule(reactContext: ReactApplicationContext?) :
6391
)
6492
return
6593
}
66-
UiThreadUtil.runOnUiThread(
67-
object : GuardedRunnable(reactApplicationContext) {
68-
override fun runGuarded() {
69-
val window = activity.window ?: return
70-
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
71-
if (animated) {
72-
val curColor = window.statusBarColor
73-
val colorAnimation = ValueAnimator.ofObject(ArgbEvaluator(), curColor, color)
74-
colorAnimation.addUpdateListener { animator ->
75-
activity.window?.statusBarColor = (animator.animatedValue as Int)
76-
}
77-
colorAnimation.setDuration(300).startDelay = 0
78-
colorAnimation.start()
79-
} else {
80-
window.statusBarColor = color
81-
}
82-
}
83-
}
84-
)
94+
UiThreadUtil.runOnUiThread { activity.window?.setStatusBarColor(color, animated) }
8595
}
8696

8797
override fun setTranslucent(translucent: Boolean) {
@@ -100,13 +110,7 @@ internal class StatusBarModule(reactContext: ReactApplicationContext?) :
100110
)
101111
return
102112
}
103-
UiThreadUtil.runOnUiThread(
104-
object : GuardedRunnable(reactApplicationContext) {
105-
override fun runGuarded() {
106-
activity.window?.setStatusBarTranslucency(translucent)
107-
}
108-
}
109-
)
113+
UiThreadUtil.runOnUiThread { activity.window?.setStatusBarTranslucency(translucent) }
110114
}
111115

112116
override fun setHidden(hidden: Boolean) {
@@ -118,10 +122,12 @@ internal class StatusBarModule(reactContext: ReactApplicationContext?) :
118122
)
119123
return
120124
}
121-
UiThreadUtil.runOnUiThread { activity.window?.setStatusBarVisibility(hidden) }
125+
UiThreadUtil.runOnUiThread {
126+
activity.window?.setStatusBarVisibility(hidden)
127+
extraWindows.forEach { it.setStatusBarVisibility(hidden) }
128+
}
122129
}
123130

124-
@Suppress("DEPRECATION")
125131
override fun setStyle(style: String?) {
126132
val activity = reactApplicationContext.getCurrentActivity()
127133
if (activity == null) {
@@ -131,36 +137,10 @@ internal class StatusBarModule(reactContext: ReactApplicationContext?) :
131137
)
132138
return
133139
}
134-
UiThreadUtil.runOnUiThread(
135-
Runnable {
136-
val window = activity.window ?: return@Runnable
137-
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
138-
val insetsController = window.insetsController ?: return@Runnable
139-
if ("dark-content" == style) {
140-
// dark-content means dark icons on a light status bar
141-
insetsController.setSystemBarsAppearance(
142-
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS,
143-
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS,
144-
)
145-
} else {
146-
insetsController.setSystemBarsAppearance(
147-
0,
148-
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS,
149-
)
150-
}
151-
} else {
152-
val decorView = window.decorView
153-
var systemUiVisibilityFlags = decorView.systemUiVisibility
154-
systemUiVisibilityFlags =
155-
if ("dark-content" == style) {
156-
systemUiVisibilityFlags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
157-
} else {
158-
systemUiVisibilityFlags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
159-
}
160-
decorView.systemUiVisibility = systemUiVisibilityFlags
161-
}
162-
}
163-
)
140+
UiThreadUtil.runOnUiThread {
141+
activity.window?.setStatusBarStyle(style)
142+
extraWindows.forEach { it.setStatusBarStyle(style) }
143+
}
164144
}
165145

166146
companion object {

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ThemedReactContext.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ package com.facebook.react.uimanager
1111

1212
import android.app.Activity
1313
import android.content.Context
14+
import android.view.Window
1415
import com.facebook.react.bridge.Callback
1516
import com.facebook.react.bridge.CatalystInstance
17+
import com.facebook.react.bridge.ExtraWindowEventListener
1618
import com.facebook.react.bridge.JavaScriptContextHolder
1719
import com.facebook.react.bridge.JavaScriptModule
1820
import com.facebook.react.bridge.LifecycleEventListener
@@ -67,6 +69,22 @@ public class ThemedReactContext(
6769
reactApplicationContext.removeLifecycleEventListener(listener)
6870
}
6971

72+
override fun addExtraWindowEventListener(listener: ExtraWindowEventListener) {
73+
reactApplicationContext.addExtraWindowEventListener(listener)
74+
}
75+
76+
override fun removeExtraWindowEventListener(listener: ExtraWindowEventListener) {
77+
reactApplicationContext.removeExtraWindowEventListener(listener)
78+
}
79+
80+
override fun onExtraWindowCreate(window: Window) {
81+
reactApplicationContext.onExtraWindowCreate(window)
82+
}
83+
84+
override fun onExtraWindowDestroy(window: Window) {
85+
reactApplicationContext.onExtraWindowDestroy(window)
86+
}
87+
7088
override fun hasCurrentActivity(): Boolean = reactApplicationContext.hasCurrentActivity()
7189

7290
override fun getCurrentActivity(): Activity? = reactApplicationContext.getCurrentActivity()

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,9 @@ public class ReactModalHostView(context: ThemedReactContext) :
196196
UiThreadUtil.assertOnUiThread()
197197

198198
dialog?.let { nonNullDialog ->
199+
nonNullDialog.window?.let { window ->
200+
(context as ThemedReactContext).onExtraWindowDestroy(window)
201+
}
199202
if (nonNullDialog.isShowing) {
200203
val dialogContext =
201204
ContextUtils.findContextOfType(nonNullDialog.context, Activity::class.java)
@@ -341,6 +344,7 @@ public class ReactModalHostView(context: ThemedReactContext) :
341344
newDialog.show()
342345
updateSystemAppearance()
343346
window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
347+
(context as ThemedReactContext).onExtraWindowCreate(window)
344348
}
345349
}
346350

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/WindowUtil.kt

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@
77

88
package com.facebook.react.views.view
99

10+
import android.animation.ArgbEvaluator
11+
import android.animation.ValueAnimator
1012
import android.graphics.Color
1113
import android.os.Build
14+
import android.view.View
1215
import android.view.Window
16+
import android.view.WindowInsetsController
1317
import android.view.WindowManager
1418
import androidx.core.view.ViewCompat
1519
import androidx.core.view.WindowCompat
@@ -37,6 +41,49 @@ public fun setEdgeToEdgeFeatureFlagOn() {
3741
isEdgeToEdgeFeatureFlagOn = true
3842
}
3943

44+
@Suppress("DEPRECATION")
45+
internal fun Window.setStatusBarColor(color: Int, animated: Boolean) {
46+
addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
47+
if (animated) {
48+
val curColor = statusBarColor
49+
val colorAnimation = ValueAnimator.ofObject(ArgbEvaluator(), curColor, color)
50+
colorAnimation.addUpdateListener { animator ->
51+
statusBarColor = (animator.animatedValue as Int)
52+
}
53+
colorAnimation.setDuration(300).startDelay = 0
54+
colorAnimation.start()
55+
} else {
56+
statusBarColor = color
57+
}
58+
}
59+
60+
@Suppress("DEPRECATION")
61+
internal fun Window.setStatusBarStyle(style: String?) {
62+
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
63+
if ("dark-content" == style) {
64+
// dark-content means dark icons on a light status bar
65+
insetsController?.setSystemBarsAppearance(
66+
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS,
67+
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS,
68+
)
69+
} else {
70+
insetsController?.setSystemBarsAppearance(
71+
0,
72+
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS,
73+
)
74+
}
75+
} else {
76+
var systemUiVisibilityFlags = decorView.systemUiVisibility
77+
systemUiVisibilityFlags =
78+
if ("dark-content" == style) {
79+
systemUiVisibilityFlags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
80+
} else {
81+
systemUiVisibilityFlags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
82+
}
83+
decorView.systemUiVisibility = systemUiVisibilityFlags
84+
}
85+
}
86+
4087
@Suppress("DEPRECATION")
4188
internal fun Window.setStatusBarTranslucency(isTranslucent: Boolean) {
4289
// If the status bar is translucent hook into the window insets calculations

0 commit comments

Comments
 (0)