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
@@ -13,9 +13,11 @@ import android.view.ViewGroup;
13
13
  import androidx.annotation.NonNull;
14
14
  import androidx.annotation.RestrictTo;
15
15
  import androidx.coordinatorlayout.widget.CoordinatorLayout;
16
- import androidx.core.graphics.Insets;
16
+ import androidx.core.view.ViewCompat;
17
17
  import androidx.core.view.WindowInsetsCompat;
18
18
 
19
+ import android.util.Log;
20
+
19
21
  import com.aurelhubert.ahbottomnavigation.AHBottomNavigation;
20
22
  import com.aurelhubert.ahbottomnavigation.AHBottomNavigationItem;
21
23
  import com.reactnativenavigation.options.BottomTabOptions;
@@ -25,6 +27,7 @@ import com.reactnativenavigation.react.CommandListener;
25
27
  import com.reactnativenavigation.react.CommandListenerAdapter;
26
28
  import com.reactnativenavigation.react.events.EventEmitter;
27
29
  import com.reactnativenavigation.utils.ImageLoader;
30
+ import com.reactnativenavigation.utils.SystemUiUtils;
28
31
  import com.reactnativenavigation.viewcontrollers.bottomtabs.attacher.BottomTabsAttacher;
29
32
  import com.reactnativenavigation.viewcontrollers.child.ChildControllersRegistry;
30
33
  import com.reactnativenavigation.viewcontrollers.parent.ParentController;
@@ -34,7 +37,9 @@ import com.reactnativenavigation.viewcontrollers.viewcontroller.ViewController;
34
37
  import com.reactnativenavigation.views.bottomtabs.BottomTabs;
35
38
  import com.reactnativenavigation.views.bottomtabs.BottomTabsContainer;
36
39
  import com.reactnativenavigation.views.bottomtabs.BottomTabsLayout;
40
+ import com.reactnativenavigation.views.bottomtabs.CustomBottomTabItemView;
37
41
 
42
+ import java.util.ArrayList;
38
43
  import java.util.Collection;
39
44
  import java.util.Deque;
40
45
  import java.util.LinkedList;
@@ -42,6 +47,8 @@ import java.util.List;
42
47
 
43
48
  public class BottomTabsController extends ParentController<BottomTabsLayout> implements AHBottomNavigation.OnTabSelectedListener, TabSelector {
44
49
 
50
+ private static final String LOG_TAG = "BottomTabsController";
51
+
45
52
  private BottomTabsContainer bottomTabsContainer;
46
53
  private BottomTabs bottomTabs;
47
54
  private final Deque<Integer> selectionStack;
@@ -51,6 +58,7 @@ public class BottomTabsController extends ParentController<BottomTabsLayout> imp
51
58
  private final BottomTabsAttacher tabsAttacher;
52
59
  private final BottomTabsPresenter presenter;
53
60
  private final BottomTabPresenter tabPresenter;
61
+ private boolean useCustomItemViews;
54
62
 
55
63
  public BottomTabsAnimator getAnimator() {
56
64
  return presenter.getAnimator();
@@ -105,13 +113,59 @@ public class BottomTabsController extends ParentController<BottomTabsLayout> imp
105
113
  bottomTabs.setOnTabSelectedListener(this);
106
114
  root.addBottomTabsContainer(bottomTabsContainer);
107
115
 
116
+ useCustomItemViews = resolveUseCustomItemViews();
117
+ tabPresenter.setUseCustomItemViews(useCustomItemViews);
118
+
108
119
  bottomTabs.addItems(createTabs());
120
+
121
+ if (useCustomItemViews) {
122
+ attachCustomItemViewsToCells();
123
+ }
124
+
109
125
  setInitialTab(resolveCurrentOptions);
110
126
  tabsAttacher.attach();
111
127
 
112
128
  return root;
113
129
  }
114
130
 
131
+ private boolean resolveUseCustomItemViews() {
132
+ if (tabs.isEmpty()) return false;
133
+ int withComponent = 0;
134
+ for (ViewController<?> tab : tabs) {
135
+ BottomTabOptions options = tab.resolveCurrentOptions(initialOptions).bottomTabOptions;
136
+ if (options.component.hasValue()) withComponent++;
137
+ }
138
+ if (withComponent == 0) return false;
139
+ if (withComponent != tabs.size()) {
140
+ Log.w(LOG_TAG,
141
+ "Mixed bottomTab.component usage detected (" + withComponent + " of "
142
+ + tabs.size() + " tabs). All tabs must declare a component or none — "
143
+ + "falling back to native rendering for all tabs.");
144
+ return false;
145
+ }
146
+ return true;
147
+ }
148
+
149
+ private void attachCustomItemViewsToCells() {
150
+ List<CustomBottomTabItemView> overlays = new ArrayList<>();
151
+ int initialIndex = bottomTabs.getCurrentItem();
152
+ for (int i = 0; i < tabs.size(); i++) {
153
+ BottomTabOptions options = tabs.get(i).resolveCurrentOptions(initialOptions).bottomTabOptions;
154
+ String componentId = options.component.componentId.get(tabs.get(i).getId() + "_tab_" + i);
155
+ String componentName = options.component.name.get();
156
+ String badge = options.badge.hasValue() ? options.badge.get() : null;
157
+ CustomBottomTabItemView itemView = new CustomBottomTabItemView(
158
+ getActivity(),
159
+ componentId,
160
+ componentName,
161
+ i,
162
+ i == initialIndex,
163
+ badge);
164
+ overlays.add(itemView);
165
+ }
166
+ bottomTabs.setCustomItemViews(overlays);
167
+ }
168
+
115
169
  private void setInitialTab(Options resolveCurrentOptions) {
116
170
  int initialTabIndex = 0;
117
171
  if (resolveCurrentOptions.bottomTabsOptions.currentTabId.hasValue())
@@ -120,6 +174,9 @@ public class BottomTabsController extends ParentController<BottomTabsLayout> imp
120
174
  initialTabIndex = resolveCurrentOptions.bottomTabsOptions.currentTabIndex.get();
121
175
  }
122
176
  bottomTabs.setCurrentItem(initialTabIndex, false);
177
+ if (useCustomItemViews) {
178
+ bottomTabs.onCustomItemViewSelectionChanged(initialTabIndex);
179
+ }
123
180
  }
124
181
 
125
182
  @NonNull
@@ -156,6 +213,7 @@ public class BottomTabsController extends ParentController<BottomTabsLayout> imp
156
213
  public void applyChildOptions(Options options, ViewController<?> child) {
157
214
  super.applyChildOptions(options, child);
158
215
  presenter.applyChildOptions(resolveCurrentOptions(), child);
216
+ onNavigationBarOptionsChanged(options);
159
217
  performOnParentController(parent -> parent.applyChildOptions(
160
218
  this.options.copy()
161
219
  .clearBottomTabsOptions()
@@ -170,6 +228,7 @@ public class BottomTabsController extends ParentController<BottomTabsLayout> imp
170
228
  super.mergeChildOptions(options, child);
171
229
  presenter.mergeChildOptions(options, child);
172
230
  tabPresenter.mergeChildOptions(options, child);
231
+ onNavigationBarOptionsChanged(options);
173
232
  performOnParentController(parent -> parent.mergeChildOptions(options.copy().clearBottomTabsOptions(), child));
174
233
  }
175
234
 
@@ -291,6 +350,9 @@ public class BottomTabsController extends ParentController<BottomTabsLayout> imp
291
350
  ViewController<?> previouslyVisible = getCurrentChild();
292
351
  bottomTabs.setCurrentItem(newIndex, false);
293
352
  getCurrentChild().onSelected(previouslyVisible);
353
+ if (useCustomItemViews) {
354
+ bottomTabs.onCustomItemViewSelectionChanged(newIndex);
355
+ }
294
356
  }
295
357
 
296
358
  private void saveTabSelection(int newIndex, boolean enableSelectionHistory) {
@@ -316,14 +378,23 @@ public class BottomTabsController extends ParentController<BottomTabsLayout> imp
316
378
 
317
379
  @Override
318
380
  protected WindowInsetsCompat onApplyWindowInsets(View view, WindowInsetsCompat insets) {
319
- Insets sysInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars());
320
- Insets imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime());
321
-
322
- int bottomInset = (imeInsets.bottom > 0) ? 0 : sysInsets.bottom;
323
- view.setPaddingRelative(0, 0, 0, bottomInset);
381
+ boolean drawBehindNavBar = resolveCurrentOptions().navigationBar.isDrawBehindAndVisible();
382
+ view.setPaddingRelative(0, 0, 0, SystemUiUtils.getBottomTabsSystemBarPadding(insets, drawBehindNavBar));
324
383
  return insets;
325
384
  }
326
385
 
386
+ private void onNavigationBarOptionsChanged(Options options) {
387
+ if (!options.navigationBar.hasAnyValue()) return;
388
+ refreshNavigationBarInsets();
389
+ }
390
+
391
+ private void refreshNavigationBarInsets() {
392
+ if (getView() != null) {
393
+ applyBottomInset();
394
+ ViewCompat.requestApplyInsets(getView());
395
+ }
396
+ }
397
+
327
398
  @RestrictTo(RestrictTo.Scope.TESTS)
328
399
  public BottomTabs getBottomTabs() {
329
400
  return bottomTabs;
@@ -393,6 +393,7 @@ class BottomTabsPresenter(
393
393
  private fun syncNavigationBarColor(options: Options, tabsColor: Int) {
394
394
  val resolved = options.copy().withDefaultOptions(defaultOptions)
395
395
  if (resolved.navigationBar.backgroundColor.hasValue()) return
396
+ if (resolved.navigationBar.shouldDrawBehind()) return
396
397
  val window = (bottomTabsContainer.context as? Activity)?.window ?: return
397
398
  SystemUiUtils.setNavigationBarBackgroundColor(window, tabsColor, isColorLight(tabsColor))
398
399
  }
@@ -173,11 +173,8 @@ public class ComponentViewController extends ChildController<ComponentLayout> {
173
173
  insets.getInsets(WindowInsetsCompat.Type.navigationBars()).top -
174
174
  systemBarsInsets.top;
175
175
 
176
- int navBarBottom = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom;
177
- int imeBottom = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom;
178
- int systemWindowInsetBottom = SystemUiUtils.isEdgeToEdgeActive()
179
- ? Math.max(imeBottom, navBarBottom)
180
- : imeBottom;
176
+ boolean drawBehindNavBar = resolveCurrentOptions(presenter.defaultOptions).navigationBar.isDrawBehindAndVisible();
177
+ int systemWindowInsetBottom = SystemUiUtils.getContentBottomSystemBarInset(insets, drawBehindNavBar);
181
178
 
182
179
  WindowInsetsCompat finalInsets = new WindowInsetsCompat.Builder()
183
180
  .setInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.ime(),
@@ -11,6 +11,8 @@ import android.view.View;
11
11
  import android.view.ViewGroup;
12
12
  import android.view.ViewGroup.MarginLayoutParams;
13
13
 
14
+ import androidx.core.view.ViewCompat;
15
+
14
16
  import com.reactnativenavigation.options.NavigationBarOptions;
15
17
  import com.reactnativenavigation.options.Options;
16
18
  import com.reactnativenavigation.options.OrientationOptions;
@@ -114,16 +116,16 @@ public class Presenter {
114
116
 
115
117
  private void applyNavigationBarOptions(NavigationBarOptions options) {
116
118
  applyNavigationBarVisibility(options);
117
- setNavigationBarBackgroundColor(options);
119
+ applyNavigationBarBackground(options);
120
+ refreshNavigationBarInsets();
118
121
  }
119
122
 
120
123
  private void mergeNavigationBarOptions(NavigationBarOptions options) {
121
- mergeNavigationBarVisibility(options);
122
- setNavigationBarBackgroundColor(options);
123
- }
124
-
125
- private void mergeNavigationBarVisibility(NavigationBarOptions options) {
126
- if (options.isVisible.hasValue()) applyNavigationBarOptions(options);
124
+ if (options.isVisible.hasValue()) {
125
+ applyNavigationBarVisibility(options);
126
+ }
127
+ applyNavigationBarBackground(options);
128
+ refreshNavigationBarInsets();
127
129
  }
128
130
 
129
131
  private void applyNavigationBarVisibility(NavigationBarOptions options) {
@@ -136,20 +138,38 @@ public class Presenter {
136
138
  }
137
139
  }
138
140
 
139
- private void setNavigationBarBackgroundColor(NavigationBarOptions navigationBar) {
141
+ private void applyNavigationBarBackground(NavigationBarOptions navigationBar) {
140
142
  if (activity == null) return;
141
143
  int defaultColor = SystemUiUtils.getDefaultNavBarColor();
142
- if (navigationBar.backgroundColor.canApplyValue()) {
143
- int color = navigationBar.backgroundColor.get(defaultColor);
144
- SystemUiUtils.setNavigationBarBackgroundColor(activity.getWindow(), color, isColorLight(color));
144
+ int color;
145
+ boolean hideOverlay;
146
+ if (navigationBar.isDrawBehindAndVisible()) {
147
+ if (navigationBar.backgroundColor.canApplyValue()) {
148
+ color = navigationBar.backgroundColor.get(defaultColor);
149
+ hideOverlay = Color.alpha(color) == 0;
150
+ } else {
151
+ color = Color.TRANSPARENT;
152
+ hideOverlay = true;
153
+ }
145
154
  } else {
146
- SystemUiUtils.setNavigationBarBackgroundColor(activity.getWindow(), defaultColor, isColorLight(defaultColor));
155
+ hideOverlay = false;
156
+ color = navigationBar.backgroundColor.canApplyValue()
157
+ ? navigationBar.backgroundColor.get(defaultColor)
158
+ : defaultColor;
147
159
  }
160
+ SystemUiUtils.setNavigationBarBackgroundColor(
161
+ activity.getWindow(), color, isColorLight(color), hideOverlay);
162
+ }
163
+
164
+ private void refreshNavigationBarInsets() {
165
+ if (activity == null) return;
166
+ ViewCompat.requestApplyInsets(activity.getWindow().getDecorView());
148
167
  }
149
168
 
150
169
  public void onConfigurationChanged(ViewController controller, Options options) {
151
170
  Options withDefault = options.withDefaultOptions(defaultOptions);
152
- setNavigationBarBackgroundColor(withDefault.navigationBar);
171
+ applyNavigationBarBackground(withDefault.navigationBar);
172
+ refreshNavigationBarInsets();
153
173
  StatusBarPresenter.instance.onConfigurationChanged(withDefault.statusBar);
154
174
  applyBackgroundColor(controller, withDefault);
155
175
  }
@@ -8,6 +8,8 @@ import android.content.Context;
8
8
  import android.graphics.Color;
9
9
  import android.graphics.drawable.Drawable;
10
10
  import android.view.View;
11
+ import android.view.ViewGroup;
12
+ import android.widget.FrameLayout;
11
13
  import android.widget.LinearLayout;
12
14
 
13
15
  import androidx.annotation.IntRange;
@@ -25,6 +27,19 @@ public class BottomTabs extends AHBottomNavigation {
25
27
  private boolean itemsCreationEnabled = true;
26
28
  private boolean shouldCreateItems = true;
27
29
  private List<Runnable> onItemCreationEnabled = new ArrayList<>();
30
+ private final List<CustomBottomTabItemView> customItemViews = new ArrayList<>();
31
+ private boolean externalCustomItemViewHost = false;
32
+
33
+ /**
34
+ * When enabled, this view stops re-parenting custom React tab item views
35
+ * into its native cells on every layout pass — the caller assumes full
36
+ * ownership of where those item views live in the view tree (used by
37
+ * the customRow floating-row implementation). Existing behavior is
38
+ * unchanged when this remains {@code false} (the default).
39
+ */
40
+ public void setExternalCustomItemViewHost(boolean enabled) {
41
+ this.externalCustomItemViewHost = enabled;
42
+ }
28
43
 
29
44
  public BottomTabs(Context context) {
30
45
  super(context);
@@ -131,6 +146,67 @@ public class BottomTabs extends AHBottomNavigation {
131
146
  if (tabsContainer != null) tabsContainer.setLayoutDirection(direction.get());
132
147
  }
133
148
 
149
+ /**
150
+ * Replace the visual content of every tab cell with the provided custom
151
+ * views. The custom view is attached as a child of the AHBottomNavigation
152
+ * cell view so taps continue to be handled by the native cell. Pass an
153
+ * empty list to remove all overlays.
154
+ */
155
+ public void setCustomItemViews(List<CustomBottomTabItemView> customViews) {
156
+ clearCustomItemViews();
157
+ if (customViews == null || customViews.isEmpty()) return;
158
+
159
+ customItemViews.addAll(customViews);
160
+ attachCustomItemViews();
161
+ }
162
+
163
+ public void onCustomItemViewSelectionChanged(int selectedIndex) {
164
+ for (int i = 0; i < customItemViews.size(); i++) {
165
+ customItemViews.get(i).setItemSelected(i == selectedIndex);
166
+ }
167
+ }
168
+
169
+ public CustomBottomTabItemView getCustomItemView(int index) {
170
+ if (index < 0 || index >= customItemViews.size()) return null;
171
+ return customItemViews.get(index);
172
+ }
173
+
174
+ public boolean hasCustomItemViews() {
175
+ return !customItemViews.isEmpty();
176
+ }
177
+
178
+ private void clearCustomItemViews() {
179
+ for (CustomBottomTabItemView view : customItemViews) {
180
+ ViewGroup parent = (ViewGroup) view.getParent();
181
+ if (parent != null) parent.removeView(view);
182
+ }
183
+ customItemViews.clear();
184
+ }
185
+
186
+ private void attachCustomItemViews() {
187
+ if (externalCustomItemViewHost) return;
188
+ for (int i = 0; i < customItemViews.size(); i++) {
189
+ View cell = getViewAtPosition(i);
190
+ if (!(cell instanceof ViewGroup)) continue;
191
+ CustomBottomTabItemView itemView = customItemViews.get(i);
192
+ ViewGroup parent = (ViewGroup) itemView.getParent();
193
+ if (parent != null && parent != cell) parent.removeView(itemView);
194
+ if (itemView.getParent() == null) {
195
+ ((ViewGroup) cell).addView(itemView, new FrameLayout.LayoutParams(
196
+ FrameLayout.LayoutParams.MATCH_PARENT,
197
+ FrameLayout.LayoutParams.MATCH_PARENT));
198
+ }
199
+ }
200
+ }
201
+
202
+ @Override
203
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
204
+ super.onLayout(changed, l, t, r, b);
205
+ if (changed && !customItemViews.isEmpty()) {
206
+ attachCustomItemViews();
207
+ }
208
+ }
209
+
134
210
  private boolean hasItemsAndIsMeasured(int w, int h, int oldw, int oldh) {
135
211
  return w != 0 && h != 0 && (w != oldw || h != oldh) && getItemsCount() > 0;
136
212
  }
@@ -0,0 +1,73 @@
1
+ package com.reactnativenavigation.views.bottomtabs
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.content.Context
5
+ import android.os.Bundle
6
+ import android.view.MotionEvent
7
+ import android.widget.FrameLayout
8
+ import com.reactnativenavigation.react.ReactView
9
+
10
+ /**
11
+ * Hosts a [ReactView] that renders a user-supplied React component as a
12
+ * bottom tab item. The view sits on top of the native AHBottomNavigation tab
13
+ * cell and forwards touches through to the underlying cell so native
14
+ * selection, ripple and `selectTabOnPress: false` keep working.
15
+ *
16
+ * The hosted component receives the following props at creation:
17
+ * `componentId`, `tabIndex`, `selected`, `badge`. Selection updates are
18
+ * pushed via [setSelected]; badge updates via [setBadge].
19
+ */
20
+ @SuppressLint("ViewConstructor")
21
+ class CustomBottomTabItemView(
22
+ context: Context,
23
+ val componentId: String,
24
+ val componentName: String,
25
+ val tabIndex: Int,
26
+ initialSelected: Boolean,
27
+ initialBadge: String?
28
+ ) : FrameLayout(context) {
29
+
30
+ val reactView: ReactView = ReactView(context, componentId, componentName)
31
+ private var isCurrentlySelected: Boolean = initialSelected
32
+ private var badge: String? = initialBadge
33
+
34
+ init {
35
+ addView(reactView)
36
+ reactView.isClickable = false
37
+ reactView.isFocusable = false
38
+ isClickable = false
39
+ isFocusable = false
40
+ pushProps()
41
+ }
42
+
43
+ /**
44
+ * Touches must always reach the underlying AHBottomNavigation cell so
45
+ * that native selection, ripple, accessibility focus and
46
+ * `selectTabOnPress: false` keep working. Returning false here makes
47
+ * this view completely transparent to touch input and prevents any
48
+ * `Touchable*` rendered inside the React tree from swallowing taps.
49
+ */
50
+ override fun dispatchTouchEvent(ev: MotionEvent?): Boolean = false
51
+
52
+ fun setItemSelected(selected: Boolean) {
53
+ if (this.isCurrentlySelected == selected) return
54
+ this.isCurrentlySelected = selected
55
+ pushProps()
56
+ }
57
+
58
+ fun setBadge(badge: String?) {
59
+ if (this.badge == badge) return
60
+ this.badge = badge
61
+ pushProps()
62
+ }
63
+
64
+ private fun pushProps() {
65
+ val bundle = Bundle().apply {
66
+ putString("componentId", componentId)
67
+ putInt("tabIndex", tabIndex)
68
+ putBoolean("selected", isCurrentlySelected)
69
+ if (badge != null) putString("badge", badge)
70
+ }
71
+ reactView.setProps(bundle)
72
+ }
73
+ }
@@ -96,4 +96,18 @@ public class PresenterTest extends BaseTest {
96
96
  verify(parentView).setPadding(2,1,4,3);
97
97
  }
98
98
 
99
+ @Test
100
+ public void applyNavigationBarDrawBehind_usesTransparentOverlay() {
101
+ mockSystemUiUtils(0, 0, (mockedStatic) -> {
102
+ ViewGroup spy = spy(new FrameLayout(activity));
103
+ Mockito.when(controller.getView()).thenReturn(spy);
104
+ Mockito.when(controller.resolveCurrentOptions()).thenReturn(Options.EMPTY);
105
+ Options options = new Options();
106
+ options.navigationBar.drawBehind = new Bool(true);
107
+ uut.applyOptions(controller, options);
108
+ mockedStatic.verify(() -> SystemUiUtils.setNavigationBarBackgroundColor(
109
+ any(), eq(android.graphics.Color.TRANSPARENT), eq(false), eq(true)), times(1));
110
+ });
111
+ }
112
+
99
113
  }
@@ -1,16 +1,28 @@
1
1
  package com.reactnativenavigation.utils
2
2
 
3
3
  import android.graphics.Color
4
+ import android.graphics.drawable.ColorDrawable
5
+ import android.view.View
4
6
  import android.view.Window
7
+ import android.widget.FrameLayout
8
+ import androidx.appcompat.app.AppCompatActivity
5
9
  import com.reactnativenavigation.BaseRobolectricTest
6
10
  import com.reactnativenavigation.utils.SystemUiUtils.STATUS_BAR_HEIGHT_TRANSLUCENCY
11
+ import org.assertj.core.api.Java6Assertions.assertThat
12
+ import org.junit.After
7
13
  import org.junit.Test
8
14
  import org.mockito.Mockito
9
15
  import org.mockito.kotlin.verify
16
+ import org.robolectric.Robolectric
10
17
  import kotlin.math.ceil
11
18
 
12
19
  class SystemUiUtilsTest : BaseRobolectricTest() {
13
20
 
21
+ @After
22
+ fun afterEach() {
23
+ SystemUiUtils.tearDown()
24
+ }
25
+
14
26
  @Test
15
27
  fun `setStatusBarColor - should change color considering alpha`() {
16
28
  val window = Mockito.mock(Window::class.java)
@@ -24,4 +36,55 @@ class SystemUiUtilsTest : BaseRobolectricTest() {
24
36
 
25
37
  verify(window).statusBarColor = Color.argb(ceil(STATUS_BAR_HEIGHT_TRANSLUCENCY*255).toInt(), 22, 255, 255)
26
38
  }
27
- }
39
+
40
+ @Test
41
+ fun `setupSystemBarBackgrounds - initializes navigation bar background from resolved color`() {
42
+ val activity = Robolectric.setupActivity(AppCompatActivity::class.java)
43
+ val contentLayout = FrameLayout(activity)
44
+ val initialColor = Color.RED
45
+ SystemUiUtils.setNavigationBarBackgroundColor(activity.window, initialColor, false)
46
+
47
+ SystemUiUtils.setupSystemBarBackgrounds(activity, contentLayout)
48
+
49
+ assertThat(getBackgroundColor(getNavigationBarBackground(contentLayout))).isEqualTo(initialColor)
50
+ }
51
+
52
+ @Test
53
+ fun `setNavigationBarBackgroundColor - updates view and window color when edge-to-edge is inactive`() {
54
+ val activity = Robolectric.setupActivity(AppCompatActivity::class.java)
55
+ val contentLayout = FrameLayout(activity)
56
+ @Suppress("DEPRECATION")
57
+ activity.window.navigationBarColor = Color.BLACK
58
+ SystemUiUtils.setupSystemBarBackgrounds(activity, contentLayout)
59
+
60
+ SystemUiUtils.setNavigationBarBackgroundColor(activity.window, Color.WHITE, true)
61
+
62
+ assertThat(getBackgroundColor(getNavigationBarBackground(contentLayout))).isEqualTo(Color.WHITE)
63
+ assertThat(activity.window.decorView.systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR)
64
+ .isEqualTo(View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR)
65
+ }
66
+
67
+ @Test
68
+ fun `setNavigationBarBackgroundColor - updates view and icon appearance when edge-to-edge is active`() {
69
+ val activity = Robolectric.setupActivity(AppCompatActivity::class.java)
70
+ val contentLayout = FrameLayout(activity)
71
+ val initialColor = Color.RED
72
+ SystemUiUtils.setNavigationBarBackgroundColor(activity.window, initialColor, false)
73
+ SystemUiUtils.setupSystemBarBackgrounds(activity, contentLayout)
74
+ SystemUiUtils.activateEdgeToEdge()
75
+
76
+ SystemUiUtils.setNavigationBarBackgroundColor(activity.window, Color.WHITE, true)
77
+
78
+ assertThat(getBackgroundColor(getNavigationBarBackground(contentLayout))).isEqualTo(Color.WHITE)
79
+ assertThat(activity.window.decorView.systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR)
80
+ .isEqualTo(View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR)
81
+ }
82
+
83
+ private fun getNavigationBarBackground(contentLayout: FrameLayout): View {
84
+ return contentLayout.getChildAt(contentLayout.childCount - 1)
85
+ }
86
+
87
+ private fun getBackgroundColor(view: View): Int {
88
+ return (view.background as ColorDrawable).color
89
+ }
90
+ }
@@ -28,6 +28,11 @@ Base class that user's AppDelegate must extend. Handles React Native and navigat
28
28
  - Creates `RCTRootViewFactory` and `ReactHost`
29
29
  - Calls `[ReactNativeNavigation bootstrapWithHost:]` to initialize navigation
30
30
  - Handles RN version differences (0.77, 0.78, 0.79+) via compile-time macros
31
+ - **Deep linking plumbing**:
32
+ - Implements `application:openURL:options:` and `application:continueUserActivity:restorationHandler:`; both call `-dispatchDeepLinkURL:`.
33
+ - `-dispatchDeepLinkURL:` posts `RCTOpenURLNotification` directly if the React runtime is ready, otherwise enqueues the URL.
34
+ - Observes `RCTContentDidAppearNotification` (the Fabric/bridgeless signal) to flush the queue. This solves the cold-start race where URLs (push notifications, OS link launches) arrive before `RCTLinkingManager` is listening.
35
+ - Subclasses can call `-dispatchDeepLinkURL:` manually from notification delegates or any other URL source; the queueing behavior is reused automatically.
31
36
 
32
37
  ### ReactNativeNavigation Bootstrap
33
38
  **File**: `ReactNativeNavigation.h/mm`
@@ -4,6 +4,13 @@
4
4
 
5
5
  @property(nonatomic, strong, readonly) RNNTabBarItemCreator *tabCreator;
6
6
 
7
+ /**
8
+ * When YES, tabs whose options declare `bottomTab.component` skip native
9
+ * icon/text/sfSymbol/role application. The accompanying
10
+ * `RNNCustomTabBarItemView` is responsible for visual rendering of the tab.
11
+ */
12
+ @property(nonatomic, assign) BOOL useCustomItemViews;
13
+
7
14
  - (instancetype)initWithDefaultOptions:(RNNNavigationOptions *)defaultOptions
8
15
  tabCreator:(RNNTabBarItemCreator *)tabCreator;
9
16
 
@@ -36,10 +36,37 @@
36
36
 
37
37
  - (void)createTabBarItem:(UIViewController *)child
38
38
  bottomTabOptions:(RNNBottomTabOptions *)bottomTabOptions {
39
+ if (_useCustomItemViews && bottomTabOptions.component.name.hasValue) {
40
+ UITabBarItem *blankItem = [self createBlankTabBarItem:child
41
+ bottomTabOptions:bottomTabOptions];
42
+ if (blankItem != child.tabBarItem) {
43
+ child.tabBarItem = blankItem;
44
+ }
45
+ return;
46
+ }
47
+
39
48
  UITabBarItem *updatedItem = [_tabCreator createTabBarItem:bottomTabOptions mergeItem:child.tabBarItem];
40
49
  if (updatedItem != child.tabBarItem) {
41
50
  child.tabBarItem = updatedItem;
42
51
  }
43
52
  }
44
53
 
54
+ // Builds a truly blank `UITabBarItem` (nil image, nil title). When custom
55
+ // item views are active, `RNNBottomTabsController` hides the native tab bar
56
+ // visuals and renders the custom row on top. The bar item still needs to
57
+ // exist so that `UITabBarController` reserves the right number of slots and
58
+ // the bottom safe-area inset.
59
+ - (UITabBarItem *)createBlankTabBarItem:(UIViewController *)child
60
+ bottomTabOptions:(RNNBottomTabOptions *)bottomTabOptions {
61
+ UITabBarItem *item = child.tabBarItem ?: [UITabBarItem new];
62
+ item.image = nil;
63
+ item.selectedImage = nil;
64
+ item.title = nil;
65
+ item.tag = bottomTabOptions.tag;
66
+ item.accessibilityIdentifier = [bottomTabOptions.testID withDefault:nil];
67
+ item.accessibilityLabel = [bottomTabOptions.accessibilityLabel withDefault:nil];
68
+ item.imageInsets = UIEdgeInsetsZero;
69
+ return item;
70
+ }
71
+
45
72
  @end
@@ -56,4 +56,20 @@
56
56
  @property(nonatomic) BOOL bridgelessEnabled;
57
57
  #endif
58
58
 
59
+ /**
60
+ * Dispatch a deep link URL through React Native's Linking module so JS
61
+ * subscribers (including RNN's built-in deep linking framework) receive it.
62
+ *
63
+ * Safe to call before the JS bridge is ready: URLs that arrive early
64
+ * (e.g. cold-start notification taps) are queued natively and flushed
65
+ * automatically once Fabric/React content first appears.
66
+ *
67
+ * Custom-scheme and universal-link openings dispatched by the OS are
68
+ * forwarded through this method automatically; call it manually only
69
+ * when your app receives a deep link from a source RNN can't intercept
70
+ * (e.g. a custom `UNUserNotificationCenterDelegate`, a third-party push
71
+ * SDK callback, etc.).
72
+ */
73
+ - (void)dispatchDeepLinkURL:(NSURL *)url;
74
+
59
75
  @end