react-native-navigation 8.8.5 → 8.8.6-snapshot.2571

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/android/src/main/java/com/reactnativenavigation/NavigationActivity.java +14 -1
  2. package/android/src/main/java/com/reactnativenavigation/NavigationApplication.java +3 -0
  3. package/android/src/main/java/com/reactnativenavigation/NavigationPackage.kt +27 -8
  4. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRow.kt +262 -0
  5. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowAttacher.kt +205 -0
  6. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowConfigStore.kt +32 -0
  7. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowLayout.kt +139 -0
  8. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowModule.kt +37 -0
  9. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowOptions.kt +68 -0
  10. package/android/src/main/java/com/reactnativenavigation/options/BottomTabOptions.java +4 -1
  11. package/android/src/main/java/com/reactnativenavigation/options/NavigationBarOptions.java +19 -1
  12. package/android/src/main/java/com/reactnativenavigation/react/ReactView.java +13 -0
  13. package/android/src/main/java/com/reactnativenavigation/react/events/ComponentType.java +2 -1
  14. package/android/src/main/java/com/reactnativenavigation/utils/SystemUiUtils.kt +63 -9
  15. package/android/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabPresenter.java +28 -0
  16. package/android/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsController.java +77 -6
  17. package/android/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsPresenter.kt +1 -0
  18. package/android/src/main/java/com/reactnativenavigation/viewcontrollers/component/ComponentViewController.java +2 -5
  19. package/android/src/main/java/com/reactnativenavigation/viewcontrollers/viewcontroller/Presenter.java +33 -13
  20. package/android/src/main/java/com/reactnativenavigation/views/bottomtabs/BottomTabs.java +76 -0
  21. package/android/src/main/java/com/reactnativenavigation/views/bottomtabs/CustomBottomTabItemView.kt +73 -0
  22. package/android/src/test/java/com/reactnativenavigation/presentation/PresenterTest.java +14 -0
  23. package/android/src/test/java/com/reactnativenavigation/utils/SystemUiUtilsTest.kt +64 -1
  24. package/ios/ARCHITECTURE.md +5 -0
  25. package/ios/BottomTabPresenter.h +7 -0
  26. package/ios/BottomTabPresenter.mm +27 -0
  27. package/ios/RNNAppDelegate.h +16 -0
  28. package/ios/RNNAppDelegate.mm +73 -0
  29. package/ios/RNNBottomTabOptions.h +2 -0
  30. package/ios/RNNBottomTabOptions.mm +5 -1
  31. package/ios/RNNBottomTabsController.h +2 -0
  32. package/ios/RNNBottomTabsController.mm +209 -1
  33. package/ios/RNNBottomTabsCustomRow.h +57 -0
  34. package/ios/RNNBottomTabsCustomRow.mm +252 -0
  35. package/ios/RNNBottomTabsCustomRowOptions.h +42 -0
  36. package/ios/RNNBottomTabsCustomRowOptions.mm +37 -0
  37. package/ios/RNNBottomTabsOptions.h +2 -0
  38. package/ios/RNNBottomTabsOptions.mm +2 -0
  39. package/ios/RNNComponentViewCreator.h +2 -1
  40. package/ios/RNNCustomTabBarItemView.h +26 -0
  41. package/ios/RNNCustomTabBarItemView.mm +83 -0
  42. package/ios/RNNReactRootViewCreator.mm +1 -0
  43. package/ios/RNNViewControllerFactory.mm +1 -0
  44. package/ios/ReactNativeNavigation.xcodeproj/project.pbxproj +24 -0
  45. package/lib/module/ARCHITECTURE.md +30 -0
  46. package/lib/module/Navigation.js +34 -1
  47. package/lib/module/Navigation.js.map +1 -1
  48. package/lib/module/NavigationDelegate.js +21 -0
  49. package/lib/module/NavigationDelegate.js.map +1 -1
  50. package/lib/module/adapters/AndroidCustomRowForwarder.js +75 -0
  51. package/lib/module/adapters/AndroidCustomRowForwarder.js.map +1 -0
  52. package/lib/module/commands/Commands.js +8 -0
  53. package/lib/module/commands/Commands.js.map +1 -1
  54. package/lib/module/index.js +1 -0
  55. package/lib/module/index.js.map +1 -1
  56. package/lib/module/interfaces/Options.js.map +1 -1
  57. package/lib/module/linking/DeferredLinkQueue.js +52 -0
  58. package/lib/module/linking/DeferredLinkQueue.js.map +1 -0
  59. package/lib/module/linking/DeferredLinkQueue.test.js +54 -0
  60. package/lib/module/linking/DeferredLinkQueue.test.js.map +1 -0
  61. package/lib/module/linking/LinkingHandler.js +139 -0
  62. package/lib/module/linking/LinkingHandler.js.map +1 -0
  63. package/lib/module/linking/LinkingHandler.test.js +384 -0
  64. package/lib/module/linking/LinkingHandler.test.js.map +1 -0
  65. package/lib/module/linking/ModalLayoutBuilder.js +56 -0
  66. package/lib/module/linking/ModalLayoutBuilder.js.map +1 -0
  67. package/lib/module/linking/ModalLayoutBuilder.test.js +154 -0
  68. package/lib/module/linking/ModalLayoutBuilder.test.js.map +1 -0
  69. package/lib/module/linking/RouteMatcher.js +104 -0
  70. package/lib/module/linking/RouteMatcher.js.map +1 -0
  71. package/lib/module/linking/RouteMatcher.test.js +164 -0
  72. package/lib/module/linking/RouteMatcher.test.js.map +1 -0
  73. package/lib/module/linking/URLParser.js +56 -0
  74. package/lib/module/linking/URLParser.js.map +1 -0
  75. package/lib/module/linking/URLParser.test.js +100 -0
  76. package/lib/module/linking/URLParser.test.js.map +1 -0
  77. package/lib/module/linking/types.js +4 -0
  78. package/lib/module/linking/types.js.map +1 -0
  79. package/lib/typescript/Navigation.d.ts +22 -0
  80. package/lib/typescript/Navigation.d.ts.map +1 -1
  81. package/lib/typescript/NavigationDelegate.d.ts +13 -0
  82. package/lib/typescript/NavigationDelegate.d.ts.map +1 -1
  83. package/lib/typescript/adapters/AndroidCustomRowForwarder.d.ts +23 -0
  84. package/lib/typescript/adapters/AndroidCustomRowForwarder.d.ts.map +1 -0
  85. package/lib/typescript/commands/Commands.d.ts +1 -0
  86. package/lib/typescript/commands/Commands.d.ts.map +1 -1
  87. package/lib/typescript/index.d.ts +1 -0
  88. package/lib/typescript/index.d.ts.map +1 -1
  89. package/lib/typescript/interfaces/Options.d.ts +90 -0
  90. package/lib/typescript/interfaces/Options.d.ts.map +1 -1
  91. package/lib/typescript/linking/DeferredLinkQueue.d.ts +26 -0
  92. package/lib/typescript/linking/DeferredLinkQueue.d.ts.map +1 -0
  93. package/lib/typescript/linking/DeferredLinkQueue.test.d.ts +2 -0
  94. package/lib/typescript/linking/DeferredLinkQueue.test.d.ts.map +1 -0
  95. package/lib/typescript/linking/LinkingHandler.d.ts +71 -0
  96. package/lib/typescript/linking/LinkingHandler.d.ts.map +1 -0
  97. package/lib/typescript/linking/LinkingHandler.test.d.ts +2 -0
  98. package/lib/typescript/linking/LinkingHandler.test.d.ts.map +1 -0
  99. package/lib/typescript/linking/ModalLayoutBuilder.d.ts +21 -0
  100. package/lib/typescript/linking/ModalLayoutBuilder.d.ts.map +1 -0
  101. package/lib/typescript/linking/ModalLayoutBuilder.test.d.ts +2 -0
  102. package/lib/typescript/linking/ModalLayoutBuilder.test.d.ts.map +1 -0
  103. package/lib/typescript/linking/RouteMatcher.d.ts +23 -0
  104. package/lib/typescript/linking/RouteMatcher.d.ts.map +1 -0
  105. package/lib/typescript/linking/RouteMatcher.test.d.ts +2 -0
  106. package/lib/typescript/linking/RouteMatcher.test.d.ts.map +1 -0
  107. package/lib/typescript/linking/URLParser.d.ts +16 -0
  108. package/lib/typescript/linking/URLParser.d.ts.map +1 -0
  109. package/lib/typescript/linking/URLParser.test.d.ts +2 -0
  110. package/lib/typescript/linking/URLParser.test.d.ts.map +1 -0
  111. package/lib/typescript/linking/types.d.ts +107 -0
  112. package/lib/typescript/linking/types.d.ts.map +1 -0
  113. package/package.json +1 -1
  114. package/src/ARCHITECTURE.md +30 -0
  115. package/src/Navigation.ts +36 -1
  116. package/src/NavigationDelegate.ts +22 -0
  117. package/src/adapters/AndroidCustomRowForwarder.ts +83 -0
  118. package/src/commands/Commands.ts +15 -0
  119. package/src/index.ts +1 -0
  120. package/src/interfaces/Options.ts +92 -0
  121. package/src/linking/DeferredLinkQueue.test.ts +60 -0
  122. package/src/linking/DeferredLinkQueue.ts +55 -0
  123. package/src/linking/LinkingHandler.test.ts +332 -0
  124. package/src/linking/LinkingHandler.ts +169 -0
  125. package/src/linking/ModalLayoutBuilder.test.ts +105 -0
  126. package/src/linking/ModalLayoutBuilder.ts +60 -0
  127. package/src/linking/RouteMatcher.test.ts +128 -0
  128. package/src/linking/RouteMatcher.ts +126 -0
  129. package/src/linking/URLParser.test.ts +105 -0
  130. package/src/linking/URLParser.ts +62 -0
  131. package/src/linking/types.ts +115 -0
@@ -0,0 +1,139 @@
1
+ package com.reactnativenavigation.customrow
2
+
3
+ import android.app.Activity
4
+ import android.os.Build
5
+ import android.util.TypedValue
6
+ import android.view.View
7
+ import android.view.ViewGroup
8
+ import com.reactnativenavigation.views.bottomtabs.BottomTabs
9
+
10
+ /**
11
+ * Resolves how the floating custom row should anchor and whether the bottom
12
+ * system-bar inset belongs inside the row or was already applied by RNN's
13
+ * [com.reactnativenavigation.viewcontrollers.bottomtabs.BottomTabsController].
14
+ *
15
+ * On most devices (including Pixel with gesture/3-button nav) RNN pads the
16
+ * bottom-tabs controller by `systemBars().bottom`, so the native bar already
17
+ * sits above the nav area — adding the same inset again creates a visible gap.
18
+ *
19
+ * With edge-to-edge (API 35+ theme opt-in) content can extend behind the nav
20
+ * bar; the row must pin to the overlay host bottom and reserve inset inside.
21
+ */
22
+ internal object BottomTabsCustomRowLayout {
23
+
24
+ enum class AnchorMode {
25
+ /** Native bar bottom is already above system bars (RNN bottom padding). */
26
+ NATIVE_BAR_ABOVE_SYSTEM_BARS,
27
+ /** Row extends to the host bottom; inset is applied inside the row. */
28
+ EDGE_TO_EDGE,
29
+ }
30
+
31
+ data class Placement(
32
+ val anchorMode: AnchorMode,
33
+ /** Inset applied inside the row for cell/chrome layout (0 when native bar already cleared it). */
34
+ val rowSafeBottomInsetPx: Int,
35
+ val left: Int,
36
+ val top: Int,
37
+ val width: Int,
38
+ val height: Int,
39
+ )
40
+
41
+ fun resolvePlacement(
42
+ activity: Activity,
43
+ row: BottomTabsCustomRow,
44
+ bottomTabs: BottomTabs,
45
+ overlayHost: ViewGroup,
46
+ navBarInsetPx: Int,
47
+ ): Placement? {
48
+ val nativeHeight = bottomTabs.height
49
+ if (nativeHeight <= 0) return null
50
+
51
+ val horizontalMargin = row.effectiveHorizontalMarginPx()
52
+ val bottomMargin = row.effectiveBottomMarginPx()
53
+
54
+ val tabLeftInHost = tabLeftRelativeToHost(bottomTabs, overlayHost)
55
+ val tabRightInHost = tabLeftInHost + bottomTabs.width
56
+
57
+ // Row is hosted on `android.R.id.content`; anchor to `BottomTabs` bottom
58
+ // (above RNN's nav-bar padding). Never use decor.height — that draws over
59
+ // the system navigation buttons.
60
+ val anchorMode = resolveAnchorMode(activity, bottomTabs, overlayHost, navBarInsetPx)
61
+ val rowSafeBottom = 0
62
+
63
+ val contentHeight = row.effectiveContentHeightPx(nativeHeight)
64
+ val totalHeight = contentHeight + bottomMargin
65
+
66
+ val left = tabLeftInHost + horizontalMargin
67
+ val width = (tabRightInHost - horizontalMargin) - left
68
+
69
+ // RNN already lays out `BottomTabs` above its bottom padding — match that
70
+ // edge. Do not subtract `navBarInsetPx` again (that was lifting the bar).
71
+ val bottom = tabBottomRelativeToHost(bottomTabs, overlayHost) - bottomMargin
72
+ val top = bottom - totalHeight
73
+
74
+ return Placement(anchorMode, rowSafeBottom, left, top, width, totalHeight)
75
+ }
76
+
77
+ fun resolveAnchorMode(
78
+ activity: Activity,
79
+ bottomTabs: BottomTabs,
80
+ overlayHost: ViewGroup,
81
+ @Suppress("UNUSED_PARAMETER") navBarInsetPx: Int,
82
+ ): AnchorMode {
83
+ // RNN's bottom-tabs host ends at `android.R.id.content` bottom while the
84
+ // system nav bar sits below that — never treat "flush with content" as
85
+ // edge-to-edge or we reserve a phantom inset and leave a white gap.
86
+ if (!isEdgeToEdgeEnabled(activity)) {
87
+ return AnchorMode.NATIVE_BAR_ABOVE_SYSTEM_BARS
88
+ }
89
+ val decor = activity.window?.decorView ?: return AnchorMode.NATIVE_BAR_ABOVE_SYSTEM_BARS
90
+ val tolerancePx = dpToPx(activity, 4f)
91
+ val tabBottomOnScreen = screenBottom(bottomTabs)
92
+ val decorBottomOnScreen = screenBottom(decor)
93
+ return if (kotlin.math.abs(tabBottomOnScreen - decorBottomOnScreen) <= tolerancePx) {
94
+ AnchorMode.EDGE_TO_EDGE
95
+ } else {
96
+ AnchorMode.NATIVE_BAR_ABOVE_SYSTEM_BARS
97
+ }
98
+ }
99
+
100
+ private fun screenBottom(view: android.view.View): Int {
101
+ val loc = IntArray(2).also(view::getLocationOnScreen)
102
+ return loc[1] + view.height
103
+ }
104
+
105
+ fun isEdgeToEdgeEnabled(activity: Activity): Boolean {
106
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
107
+ return false
108
+ }
109
+ val typedValue = TypedValue()
110
+ val resolved = activity.theme.resolveAttribute(
111
+ android.R.attr.windowOptOutEdgeToEdgeEnforcement,
112
+ typedValue,
113
+ true
114
+ )
115
+ return resolved &&
116
+ typedValue.type == TypedValue.TYPE_INT_BOOLEAN &&
117
+ typedValue.data == 0
118
+ }
119
+
120
+ private fun tabLeftRelativeToHost(bottomTabs: BottomTabs, overlayHost: ViewGroup): Int {
121
+ val tabLoc = IntArray(2).also(bottomTabs::getLocationOnScreen)
122
+ val hostLoc = IntArray(2).also(overlayHost::getLocationOnScreen)
123
+ return tabLoc[0] - hostLoc[0]
124
+ }
125
+
126
+ private fun tabBottomRelativeToHost(bottomTabs: BottomTabs, overlayHost: ViewGroup): Int {
127
+ val tabLoc = IntArray(2).also(bottomTabs::getLocationOnScreen)
128
+ val hostLoc = IntArray(2).also(overlayHost::getLocationOnScreen)
129
+ val tabTopInHost = tabLoc[1] - hostLoc[1]
130
+ return tabTopInHost + bottomTabs.height
131
+ }
132
+
133
+ private fun dpToPx(activity: Activity, dp: Float): Int =
134
+ TypedValue.applyDimension(
135
+ TypedValue.COMPLEX_UNIT_DIP,
136
+ dp,
137
+ activity.resources.displayMetrics
138
+ ).toInt()
139
+ }
@@ -0,0 +1,37 @@
1
+ package com.reactnativenavigation.customrow
2
+
3
+ import android.app.Application
4
+ import com.facebook.react.bridge.ReactApplicationContext
5
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
6
+ import com.facebook.react.bridge.ReactMethod
7
+ import com.facebook.react.bridge.ReadableMap
8
+ import com.facebook.react.module.annotations.ReactModule
9
+
10
+ /**
11
+ * RN bridge module that lets JS push the latest `bottomTabs.customRow`
12
+ * configuration to native. The JS-side `AndroidCustomRowForwarder`
13
+ * scans `Navigation.setRoot` / `setDefaultOptions` / `mergeOptions`
14
+ * payloads and calls [configure] whenever it finds a `customRow` block.
15
+ */
16
+ @ReactModule(name = BottomTabsCustomRowModule.NAME)
17
+ class BottomTabsCustomRowModule(
18
+ reactContext: ReactApplicationContext,
19
+ ) : ReactContextBaseJavaModule(reactContext) {
20
+
21
+ init {
22
+ val app = reactContext.applicationContext as? Application
23
+ if (app != null) BottomTabsCustomRowAttacher.registerOnce(app)
24
+ }
25
+
26
+ override fun getName(): String = NAME
27
+
28
+ @ReactMethod
29
+ fun configure(config: ReadableMap?) {
30
+ BottomTabsCustomRowConfigStore.update(BottomTabsCustomRowOptions.fromMap(config))
31
+ BottomTabsCustomRowAttacher.rescan()
32
+ }
33
+
34
+ companion object {
35
+ const val NAME = "RNNBottomTabsCustomRowModule"
36
+ }
37
+ }
@@ -0,0 +1,68 @@
1
+ package com.reactnativenavigation.customrow
2
+
3
+ import android.graphics.Color
4
+ import com.facebook.react.bridge.ReadableMap
5
+
6
+ /**
7
+ * Mirrors the iOS-side `RNNBottomTabsCustomRowOptions` data shape. All
8
+ * fields are optional. Defaults are chosen to give an Android equivalent
9
+ * of the iOS 26 floating glass pill on Android 12+ (RenderEffect blur),
10
+ * and an opaque material chrome on older versions.
11
+ */
12
+ data class BottomTabsCustomRowOptions(
13
+ val height: Float? = null,
14
+ val backgroundColor: Int? = null,
15
+ val backgroundEffect: BackgroundEffect? = null,
16
+ val cornerRadius: Float? = null,
17
+ val horizontalMargin: Float? = null,
18
+ val bottomMargin: Float? = null,
19
+ ) {
20
+ enum class BackgroundEffect { Glass, Blur, None }
21
+
22
+ companion object {
23
+ fun fromMap(map: ReadableMap?): BottomTabsCustomRowOptions {
24
+ if (map == null) return BottomTabsCustomRowOptions()
25
+ return BottomTabsCustomRowOptions(
26
+ height = map.optFloat("height"),
27
+ backgroundColor = map.optColor("backgroundColor"),
28
+ backgroundEffect = map.optEffect("backgroundEffect"),
29
+ cornerRadius = map.optFloat("cornerRadius"),
30
+ horizontalMargin = map.optFloat("horizontalMargin"),
31
+ bottomMargin = map.optFloat("bottomMargin"),
32
+ )
33
+ }
34
+
35
+ private fun ReadableMap.optFloat(key: String): Float? {
36
+ if (!hasKey(key) || isNull(key)) return null
37
+ return getDouble(key).toFloat()
38
+ }
39
+
40
+ private fun ReadableMap.optColor(key: String): Int? {
41
+ if (!hasKey(key) || isNull(key)) return null
42
+ // Color may arrive as a number (Android-style int) or a wrapped
43
+ // theme-color object. Support both shallowly.
44
+ return when (getType(key)) {
45
+ com.facebook.react.bridge.ReadableType.Number -> getInt(key)
46
+ com.facebook.react.bridge.ReadableType.Map -> {
47
+ val sub = getMap(key)
48
+ val light = sub?.let { if (it.hasKey("light")) it.getInt("light") else null }
49
+ light ?: sub?.let { if (it.hasKey("color")) it.getInt("color") else null }
50
+ }
51
+ com.facebook.react.bridge.ReadableType.String -> {
52
+ runCatching { Color.parseColor(getString(key)) }.getOrNull()
53
+ }
54
+ else -> null
55
+ }
56
+ }
57
+
58
+ private fun ReadableMap.optEffect(key: String): BackgroundEffect? {
59
+ if (!hasKey(key) || isNull(key)) return null
60
+ return when (getString(key)?.lowercase()) {
61
+ "glass" -> BackgroundEffect.Glass
62
+ "blur" -> BackgroundEffect.Blur
63
+ "none" -> BackgroundEffect.None
64
+ else -> null
65
+ }
66
+ }
67
+ }
68
+ }
@@ -44,6 +44,7 @@ public class BottomTabOptions {
44
44
  options.dotIndicator = DotIndicatorOptions.parse(context, json.optJSONObject("dotIndicator"));
45
45
  options.selectTabOnPress = BoolParser.parse(json, "selectTabOnPress");
46
46
  options.popToRoot = BoolParser.parse(json, "popToRoot");
47
+ options.component = ComponentOptions.parse(json.optJSONObject("component"));
47
48
 
48
49
  return options;
49
50
  }
@@ -67,6 +68,7 @@ public class BottomTabOptions {
67
68
  public Bool selectTabOnPress = new NullBool();
68
69
  public Bool popToRoot = new NullBool();
69
70
  public FontOptions font = new FontOptions();
71
+ public ComponentOptions component = new ComponentOptions();
70
72
 
71
73
 
72
74
  void mergeWith(final BottomTabOptions other) {
@@ -90,6 +92,7 @@ public class BottomTabOptions {
90
92
  if (other.dotIndicator.hasValue()) dotIndicator = other.dotIndicator;
91
93
  if (other.selectTabOnPress.hasValue()) selectTabOnPress = other.selectTabOnPress;
92
94
  if (other.popToRoot.hasValue()) popToRoot = other.popToRoot;
95
+ if (other.component.hasValue()) component = other.component;
93
96
  }
94
97
 
95
98
  void mergeWithDefault(final BottomTabOptions defaultOptions) {
@@ -113,7 +116,7 @@ public class BottomTabOptions {
113
116
  if (!dotIndicator.hasValue()) dotIndicator = defaultOptions.dotIndicator;
114
117
  if (!selectTabOnPress.hasValue()) selectTabOnPress = defaultOptions.selectTabOnPress;
115
118
  if (!popToRoot.hasValue()) popToRoot = defaultOptions.popToRoot;
116
-
119
+ if (!component.hasValue()) component = defaultOptions.component;
117
120
  }
118
121
 
119
122
  }
@@ -17,20 +17,38 @@ public class NavigationBarOptions {
17
17
 
18
18
  result.backgroundColor = ThemeColour.parse(context, json.optJSONObject("backgroundColor"));
19
19
  result.isVisible = BoolParser.parse(json, "visible");
20
+ result.drawBehind = BoolParser.parse(json, "drawBehind");
20
21
 
21
22
  return result;
22
23
  }
23
24
 
24
25
  public ThemeColour backgroundColor = new NullThemeColour();
25
26
  public Bool isVisible = new NullBool();
27
+ public Bool drawBehind = new NullBool();
26
28
 
27
29
  public void mergeWith(NavigationBarOptions other) {
28
30
  if (other.isVisible.hasValue()) isVisible = other.isVisible;
29
31
  if (other.backgroundColor.hasValue()) backgroundColor = other.backgroundColor;
32
+ if (other.drawBehind.hasValue()) drawBehind = other.drawBehind;
30
33
  }
31
34
 
32
35
  public void mergeWithDefault(NavigationBarOptions defaultOptions) {
33
36
  if (!isVisible.hasValue()) isVisible = defaultOptions.isVisible;
34
37
  if (!backgroundColor.hasValue()) backgroundColor = defaultOptions.backgroundColor;
38
+ if (!drawBehind.hasValue()) drawBehind = defaultOptions.drawBehind;
35
39
  }
36
- }
40
+
41
+ public boolean shouldDrawBehind() {
42
+ if (drawBehind.isFalse()) return false;
43
+ if (drawBehind.isTrue()) return true;
44
+ return backgroundColor.hasTransparency();
45
+ }
46
+
47
+ public boolean isDrawBehindAndVisible() {
48
+ return shouldDrawBehind() && isVisible.isTrueOrUndefined();
49
+ }
50
+
51
+ public boolean hasAnyValue() {
52
+ return isVisible.hasValue() || drawBehind.hasValue() || backgroundColor.hasValue();
53
+ }
54
+ }
@@ -16,6 +16,7 @@ import com.facebook.react.ReactApplication;
16
16
  import com.facebook.react.ReactHost;
17
17
  import com.facebook.react.bridge.ReactContext;
18
18
  import com.facebook.react.interfaces.fabric.ReactSurface;
19
+ import com.facebook.react.runtime.ReactSurfaceImpl;
19
20
  import com.facebook.react.uimanager.UIManagerHelper;
20
21
  import com.facebook.react.uimanager.common.UIManagerType;
21
22
  import com.facebook.react.uimanager.events.EventDispatcher;
@@ -71,6 +72,18 @@ public class ReactView extends FrameLayout implements IReactView, Renderable {
71
72
  reactSurface.stop();
72
73
  }
73
74
 
75
+ /**
76
+ * Replace the surface's initial props. Useful for components that need to
77
+ * receive runtime updates from native (e.g. bottom tab item components).
78
+ * No-op when the underlying surface implementation does not support
79
+ * runtime prop updates.
80
+ */
81
+ public void setProps(Bundle props) {
82
+ if (reactSurface instanceof ReactSurfaceImpl) {
83
+ ((ReactSurfaceImpl) reactSurface).updateInitProps(props);
84
+ }
85
+ }
86
+
74
87
  public void sendComponentWillStart(ComponentType type) {
75
88
  this.post(() -> {
76
89
  ReactContext currentReactContext = getReactContext();
@@ -4,7 +4,8 @@ public enum ComponentType {
4
4
  Component("Component"),
5
5
  Button("TopBarButton"),
6
6
  Title("TopBarTitle"),
7
- Background("TopBarBackground");
7
+ Background("TopBarBackground"),
8
+ BottomTabItem("BottomTabItem");
8
9
 
9
10
  private String name;
10
11
 
@@ -4,6 +4,7 @@ import android.app.Activity
4
4
  import android.graphics.Color
5
5
  import android.graphics.Rect
6
6
  import android.graphics.drawable.ColorDrawable
7
+ import android.os.Build
7
8
  import android.view.Gravity
8
9
  import android.view.View
9
10
  import android.view.ViewGroup
@@ -15,6 +16,7 @@ import androidx.core.view.WindowInsetsCompat
15
16
  import androidx.core.view.WindowInsetsControllerCompat
16
17
  import kotlin.math.abs
17
18
  import kotlin.math.ceil
19
+ import kotlin.math.max
18
20
 
19
21
  object SystemUiUtils {
20
22
  private const val STATUS_BAR_HEIGHT_M = 24
@@ -79,7 +81,7 @@ object SystemUiUtils {
79
81
  @JvmStatic
80
82
  fun setupSystemBarBackgrounds(activity: Activity, contentLayout: ViewGroup) {
81
83
  setupStatusBarBackground(activity)
82
- setupNavigationBarBackground(contentLayout)
84
+ setupNavigationBarBackground(activity.window, contentLayout)
83
85
  }
84
86
 
85
87
  private fun setupStatusBarBackground(activity: Activity) {
@@ -117,10 +119,10 @@ object SystemUiUtils {
117
119
  return view
118
120
  }
119
121
 
120
- private fun setupNavigationBarBackground(contentLayout: ViewGroup) {
122
+ private fun setupNavigationBarBackground(window: Window?, contentLayout: ViewGroup) {
121
123
  if (navBarBackgroundView != null) return
122
124
  val view = View(contentLayout.context).apply {
123
- setBackgroundColor(Color.BLACK)
125
+ setBackgroundColor(getNavigationBarBackgroundColor(window))
124
126
  }
125
127
  val params = FrameLayout.LayoutParams(
126
128
  FrameLayout.LayoutParams.MATCH_PARENT, 0, Gravity.BOTTOM
@@ -134,7 +136,7 @@ object SystemUiUtils {
134
136
  val wasThreeButton = isThreeButtonNav
135
137
  isThreeButtonNav = tappableHeight > 0
136
138
  if (isThreeButtonNav != wasThreeButton) {
137
- val color = lastExplicitNavBarColor ?: getDefaultNavBarColor()
139
+ val color = lastExplicitNavBarColor ?: getNavigationBarBackgroundColor(v)
138
140
  v.setBackgroundColor(color)
139
141
  }
140
142
  val lp = v.layoutParams
@@ -147,6 +149,17 @@ object SystemUiUtils {
147
149
  view.requestApplyInsets()
148
150
  }
149
151
 
152
+ private fun getNavigationBarBackgroundColor(window: Window?): Int {
153
+ lastExplicitNavBarColor?.let { return it }
154
+ @Suppress("DEPRECATION")
155
+ return window?.navigationBarColor ?: getDefaultNavBarColor()
156
+ }
157
+
158
+ private fun getNavigationBarBackgroundColor(view: View): Int {
159
+ lastExplicitNavBarColor?.let { return it }
160
+ return (view.background as? ColorDrawable)?.color ?: getDefaultNavBarColor()
161
+ }
162
+
150
163
  /**
151
164
  * Returns the default navigation bar color, applying 80% opacity for 3-button navigation.
152
165
  * Gesture navigation gets a fully opaque color since the bar is minimal.
@@ -176,6 +189,29 @@ object SystemUiUtils {
176
189
  isEdgeToEdgeActive = true
177
190
  }
178
191
 
192
+ @JvmStatic
193
+ fun setNavigationBarContrastEnforced(window: Window?, enforced: Boolean) {
194
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
195
+ window?.isNavigationBarContrastEnforced = enforced
196
+ }
197
+ }
198
+
199
+ @JvmStatic
200
+ fun getContentBottomSystemBarInset(insets: WindowInsetsCompat, drawBehindNavigationBar: Boolean): Int {
201
+ val imeBottom = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
202
+ if (!isEdgeToEdgeActive) return imeBottom
203
+ if (drawBehindNavigationBar) return imeBottom
204
+ val navBarBottom = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
205
+ return max(imeBottom, navBarBottom)
206
+ }
207
+
208
+ @JvmStatic
209
+ fun getBottomTabsSystemBarPadding(insets: WindowInsetsCompat, drawBehindNavigationBar: Boolean): Int {
210
+ if (insets.getInsets(WindowInsetsCompat.Type.ime()).bottom > 0) return 0
211
+ if (isEdgeToEdgeActive && drawBehindNavigationBar) return 0
212
+ return insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
213
+ }
214
+
179
215
  /**
180
216
  * Clears references to system bar background views.
181
217
  * Call from Activity.onDestroy to avoid leaking views across activity recreation.
@@ -316,18 +352,36 @@ object SystemUiUtils {
316
352
  * falls back to the deprecated window API on older configurations.
317
353
  */
318
354
  @JvmStatic
319
- fun setNavigationBarBackgroundColor(window: Window?, color: Int, lightColor: Boolean) {
320
- lastExplicitNavBarColor = color
355
+ fun setNavigationBarBackgroundColor(window: Window?, color: Int, lightColor: Boolean, hideOverlay: Boolean) {
321
356
  window?.let {
322
357
  WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightNavigationBars = lightColor
323
358
  }
324
- if (isEdgeToEdgeActive) {
325
- navBarBackgroundView?.setBackgroundColor(color)
326
- } else {
359
+ if (hideOverlay) {
360
+ lastExplicitNavBarColor = null
361
+ navBarBackgroundView?.apply {
362
+ visibility = View.GONE
363
+ setBackgroundColor(Color.TRANSPARENT)
364
+ }
365
+ @Suppress("DEPRECATION")
366
+ window?.navigationBarColor = Color.TRANSPARENT
367
+ return
368
+ }
369
+
370
+ lastExplicitNavBarColor = color
371
+ navBarBackgroundView?.apply {
372
+ visibility = View.VISIBLE
373
+ setBackgroundColor(color)
374
+ }
375
+ if (!isEdgeToEdgeActive) {
327
376
  @Suppress("DEPRECATION")
328
377
  window?.navigationBarColor = color
329
378
  }
330
379
  }
331
380
 
381
+ @JvmStatic
382
+ fun setNavigationBarBackgroundColor(window: Window?, color: Int, lightColor: Boolean) {
383
+ setNavigationBarBackgroundColor(window, color, lightColor, Color.alpha(color) == 0)
384
+ }
385
+
332
386
  // endregion
333
387
  }
@@ -20,7 +20,9 @@ import com.reactnativenavigation.utils.ImageLoadingListenerAdapter;
20
20
  import com.reactnativenavigation.utils.LateInit;
21
21
  import com.reactnativenavigation.viewcontrollers.viewcontroller.ViewController;
22
22
  import com.reactnativenavigation.views.bottomtabs.BottomTabs;
23
+ import com.reactnativenavigation.views.bottomtabs.CustomBottomTabItemView;
23
24
 
25
+ import java.util.ArrayList;
24
26
  import java.util.List;
25
27
 
26
28
  public class BottomTabPresenter {
@@ -33,6 +35,7 @@ public class BottomTabPresenter {
33
35
  private final LateInit<BottomTabs> bottomTabs = new LateInit<>();
34
36
  private final List<ViewController<?>> tabs;
35
37
  private final int defaultDotIndicatorSize;
38
+ private boolean useCustomItemViews;
36
39
 
37
40
  public BottomTabPresenter(Context context, List<ViewController<?>> tabs, ImageLoader imageLoader, TypefaceLoader typefaceLoader, Options defaultOptions) {
38
41
  this.tabs = tabs;
@@ -53,10 +56,27 @@ public class BottomTabPresenter {
53
56
  this.bottomTabs.set(bottomTabs);
54
57
  }
55
58
 
59
+ /**
60
+ * When `true`, tabs whose options declare `bottomTab.component` are
61
+ * skipped during native icon/text application. The accompanying
62
+ * `CustomBottomTabItemView` overlay is responsible for visual rendering.
63
+ */
64
+ public void setUseCustomItemViews(boolean useCustomItemViews) {
65
+ this.useCustomItemViews = useCustomItemViews;
66
+ }
67
+
56
68
  public void applyOptions() {
57
69
  bottomTabs.perform(bottomTabs -> {
58
70
  for (int i = 0; i < tabs.size(); i++) {
59
71
  BottomTabOptions tab = tabs.get(i).resolveCurrentOptions(defaultOptions).bottomTabOptions;
72
+ if (useCustomItemViews && tab.component.hasValue()) {
73
+ if (tab.testId.hasValue()) bottomTabs.setTag(i, tab.testId.get());
74
+ if (tab.badge.hasValue()) {
75
+ CustomBottomTabItemView v = bottomTabs.getCustomItemView(i);
76
+ if (v != null) v.setBadge(tab.badge.get(""));
77
+ }
78
+ continue;
79
+ }
60
80
  bottomTabs.setIconWidth(i, tab.iconWidth.get(null));
61
81
  bottomTabs.setIconHeight(i, tab.iconHeight.get(null));
62
82
  bottomTabs.setTitleTypeface(i, tab.font.getTypeface(typefaceLoader, defaultTypeface));
@@ -86,6 +106,14 @@ public class BottomTabPresenter {
86
106
  int index = bottomTabFinder.findByControllerId(child.getId());
87
107
  if (index >= 0) {
88
108
  BottomTabOptions tab = options.bottomTabOptions;
109
+ if (useCustomItemViews && bottomTabs.getCustomItemView(index) != null) {
110
+ if (tab.badge.hasValue()) {
111
+ CustomBottomTabItemView v = bottomTabs.getCustomItemView(index);
112
+ if (v != null) v.setBadge(tab.badge.get(""));
113
+ }
114
+ if (tab.testId.hasValue()) bottomTabs.setTag(index, tab.testId.get());
115
+ return;
116
+ }
89
117
  if (tab.iconWidth.hasValue()) bottomTabs.setIconWidth(index, tab.iconWidth.get(null));
90
118
  if (tab.iconHeight.hasValue()) bottomTabs.setIconHeight(index, tab.iconHeight.get(null));
91
119
  if (tab.font.hasValue()) bottomTabs.setTitleTypeface(index, tab.font.getTypeface(typefaceLoader, defaultTypeface));