react-native-navigation 8.8.6 → 8.8.7-snapshot.2601

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 (126) hide show
  1. package/android/src/main/java/com/reactnativenavigation/NavigationApplication.java +3 -0
  2. package/android/src/main/java/com/reactnativenavigation/NavigationPackage.kt +27 -8
  3. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRow.kt +262 -0
  4. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowAttacher.kt +205 -0
  5. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowConfigStore.kt +32 -0
  6. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowLayout.kt +139 -0
  7. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowModule.kt +37 -0
  8. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowOptions.kt +68 -0
  9. package/android/src/main/java/com/reactnativenavigation/options/BottomTabOptions.java +4 -1
  10. package/android/src/main/java/com/reactnativenavigation/react/ReactView.java +13 -0
  11. package/android/src/main/java/com/reactnativenavigation/react/events/ComponentType.java +2 -1
  12. package/android/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabPresenter.java +28 -0
  13. package/android/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsController.java +59 -0
  14. package/android/src/main/java/com/reactnativenavigation/views/bottomtabs/BottomTabs.java +76 -0
  15. package/android/src/main/java/com/reactnativenavigation/views/bottomtabs/CustomBottomTabItemView.kt +73 -0
  16. package/android/src/main/java/com/reactnativenavigation/views/stack/topbar/titlebar/TitleBarReactButtonView.java +54 -12
  17. package/android/src/test/java/com/reactnativenavigation/views/TitleAndButtonsContainerTest.kt +15 -1
  18. package/android/src/test/java/com/reactnativenavigation/views/TitleBarReactButtonViewTest.java +199 -0
  19. package/ios/ARCHITECTURE.md +5 -0
  20. package/ios/BottomTabPresenter.h +7 -0
  21. package/ios/BottomTabPresenter.mm +27 -0
  22. package/ios/RNNAppDelegate.h +16 -0
  23. package/ios/RNNAppDelegate.mm +73 -0
  24. package/ios/RNNBottomTabOptions.h +2 -0
  25. package/ios/RNNBottomTabOptions.mm +5 -1
  26. package/ios/RNNBottomTabsController.h +2 -0
  27. package/ios/RNNBottomTabsController.mm +209 -1
  28. package/ios/RNNBottomTabsCustomRow.h +57 -0
  29. package/ios/RNNBottomTabsCustomRow.mm +252 -0
  30. package/ios/RNNBottomTabsCustomRowOptions.h +42 -0
  31. package/ios/RNNBottomTabsCustomRowOptions.mm +37 -0
  32. package/ios/RNNBottomTabsOptions.h +2 -0
  33. package/ios/RNNBottomTabsOptions.mm +2 -0
  34. package/ios/RNNComponentViewCreator.h +2 -1
  35. package/ios/RNNCustomTabBarItemView.h +26 -0
  36. package/ios/RNNCustomTabBarItemView.mm +83 -0
  37. package/ios/RNNReactRootViewCreator.mm +1 -0
  38. package/ios/RNNViewControllerFactory.mm +1 -0
  39. package/ios/ReactNativeNavigation.xcodeproj/project.pbxproj +24 -0
  40. package/lib/module/ARCHITECTURE.md +30 -0
  41. package/lib/module/Navigation.js +34 -1
  42. package/lib/module/Navigation.js.map +1 -1
  43. package/lib/module/NavigationDelegate.js +21 -0
  44. package/lib/module/NavigationDelegate.js.map +1 -1
  45. package/lib/module/adapters/AndroidCustomRowForwarder.js +75 -0
  46. package/lib/module/adapters/AndroidCustomRowForwarder.js.map +1 -0
  47. package/lib/module/commands/Commands.js +8 -0
  48. package/lib/module/commands/Commands.js.map +1 -1
  49. package/lib/module/index.js +1 -0
  50. package/lib/module/index.js.map +1 -1
  51. package/lib/module/interfaces/Options.js.map +1 -1
  52. package/lib/module/linking/DeferredLinkQueue.js +52 -0
  53. package/lib/module/linking/DeferredLinkQueue.js.map +1 -0
  54. package/lib/module/linking/DeferredLinkQueue.test.js +54 -0
  55. package/lib/module/linking/DeferredLinkQueue.test.js.map +1 -0
  56. package/lib/module/linking/LinkingHandler.js +139 -0
  57. package/lib/module/linking/LinkingHandler.js.map +1 -0
  58. package/lib/module/linking/LinkingHandler.test.js +384 -0
  59. package/lib/module/linking/LinkingHandler.test.js.map +1 -0
  60. package/lib/module/linking/ModalLayoutBuilder.js +56 -0
  61. package/lib/module/linking/ModalLayoutBuilder.js.map +1 -0
  62. package/lib/module/linking/ModalLayoutBuilder.test.js +154 -0
  63. package/lib/module/linking/ModalLayoutBuilder.test.js.map +1 -0
  64. package/lib/module/linking/RouteMatcher.js +104 -0
  65. package/lib/module/linking/RouteMatcher.js.map +1 -0
  66. package/lib/module/linking/RouteMatcher.test.js +164 -0
  67. package/lib/module/linking/RouteMatcher.test.js.map +1 -0
  68. package/lib/module/linking/URLParser.js +56 -0
  69. package/lib/module/linking/URLParser.js.map +1 -0
  70. package/lib/module/linking/URLParser.test.js +100 -0
  71. package/lib/module/linking/URLParser.test.js.map +1 -0
  72. package/lib/module/linking/types.js +4 -0
  73. package/lib/module/linking/types.js.map +1 -0
  74. package/lib/typescript/Navigation.d.ts +22 -0
  75. package/lib/typescript/Navigation.d.ts.map +1 -1
  76. package/lib/typescript/NavigationDelegate.d.ts +13 -0
  77. package/lib/typescript/NavigationDelegate.d.ts.map +1 -1
  78. package/lib/typescript/adapters/AndroidCustomRowForwarder.d.ts +23 -0
  79. package/lib/typescript/adapters/AndroidCustomRowForwarder.d.ts.map +1 -0
  80. package/lib/typescript/commands/Commands.d.ts +1 -0
  81. package/lib/typescript/commands/Commands.d.ts.map +1 -1
  82. package/lib/typescript/index.d.ts +1 -0
  83. package/lib/typescript/index.d.ts.map +1 -1
  84. package/lib/typescript/interfaces/Options.d.ts +85 -0
  85. package/lib/typescript/interfaces/Options.d.ts.map +1 -1
  86. package/lib/typescript/linking/DeferredLinkQueue.d.ts +26 -0
  87. package/lib/typescript/linking/DeferredLinkQueue.d.ts.map +1 -0
  88. package/lib/typescript/linking/DeferredLinkQueue.test.d.ts +2 -0
  89. package/lib/typescript/linking/DeferredLinkQueue.test.d.ts.map +1 -0
  90. package/lib/typescript/linking/LinkingHandler.d.ts +71 -0
  91. package/lib/typescript/linking/LinkingHandler.d.ts.map +1 -0
  92. package/lib/typescript/linking/LinkingHandler.test.d.ts +2 -0
  93. package/lib/typescript/linking/LinkingHandler.test.d.ts.map +1 -0
  94. package/lib/typescript/linking/ModalLayoutBuilder.d.ts +21 -0
  95. package/lib/typescript/linking/ModalLayoutBuilder.d.ts.map +1 -0
  96. package/lib/typescript/linking/ModalLayoutBuilder.test.d.ts +2 -0
  97. package/lib/typescript/linking/ModalLayoutBuilder.test.d.ts.map +1 -0
  98. package/lib/typescript/linking/RouteMatcher.d.ts +23 -0
  99. package/lib/typescript/linking/RouteMatcher.d.ts.map +1 -0
  100. package/lib/typescript/linking/RouteMatcher.test.d.ts +2 -0
  101. package/lib/typescript/linking/RouteMatcher.test.d.ts.map +1 -0
  102. package/lib/typescript/linking/URLParser.d.ts +16 -0
  103. package/lib/typescript/linking/URLParser.d.ts.map +1 -0
  104. package/lib/typescript/linking/URLParser.test.d.ts +2 -0
  105. package/lib/typescript/linking/URLParser.test.d.ts.map +1 -0
  106. package/lib/typescript/linking/types.d.ts +107 -0
  107. package/lib/typescript/linking/types.d.ts.map +1 -0
  108. package/package.json +1 -1
  109. package/src/ARCHITECTURE.md +30 -0
  110. package/src/Navigation.ts +36 -1
  111. package/src/NavigationDelegate.ts +22 -0
  112. package/src/adapters/AndroidCustomRowForwarder.ts +83 -0
  113. package/src/commands/Commands.ts +15 -0
  114. package/src/index.ts +1 -0
  115. package/src/interfaces/Options.ts +87 -0
  116. package/src/linking/DeferredLinkQueue.test.ts +60 -0
  117. package/src/linking/DeferredLinkQueue.ts +55 -0
  118. package/src/linking/LinkingHandler.test.ts +332 -0
  119. package/src/linking/LinkingHandler.ts +169 -0
  120. package/src/linking/ModalLayoutBuilder.test.ts +105 -0
  121. package/src/linking/ModalLayoutBuilder.ts +60 -0
  122. package/src/linking/RouteMatcher.test.ts +128 -0
  123. package/src/linking/RouteMatcher.ts +126 -0
  124. package/src/linking/URLParser.test.ts +105 -0
  125. package/src/linking/URLParser.ts +62 -0
  126. 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
  }
@@ -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
 
@@ -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));
@@ -16,6 +16,8 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout;
16
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;
@@ -35,7 +37,9 @@ import com.reactnativenavigation.viewcontrollers.viewcontroller.ViewController;
35
37
  import com.reactnativenavigation.views.bottomtabs.BottomTabs;
36
38
  import com.reactnativenavigation.views.bottomtabs.BottomTabsContainer;
37
39
  import com.reactnativenavigation.views.bottomtabs.BottomTabsLayout;
40
+ import com.reactnativenavigation.views.bottomtabs.CustomBottomTabItemView;
38
41
 
42
+ import java.util.ArrayList;
39
43
  import java.util.Collection;
40
44
  import java.util.Deque;
41
45
  import java.util.LinkedList;
@@ -43,6 +47,8 @@ import java.util.List;
43
47
 
44
48
  public class BottomTabsController extends ParentController<BottomTabsLayout> implements AHBottomNavigation.OnTabSelectedListener, TabSelector {
45
49
 
50
+ private static final String LOG_TAG = "BottomTabsController";
51
+
46
52
  private BottomTabsContainer bottomTabsContainer;
47
53
  private BottomTabs bottomTabs;
48
54
  private final Deque<Integer> selectionStack;
@@ -52,6 +58,7 @@ public class BottomTabsController extends ParentController<BottomTabsLayout> imp
52
58
  private final BottomTabsAttacher tabsAttacher;
53
59
  private final BottomTabsPresenter presenter;
54
60
  private final BottomTabPresenter tabPresenter;
61
+ private boolean useCustomItemViews;
55
62
 
56
63
  public BottomTabsAnimator getAnimator() {
57
64
  return presenter.getAnimator();
@@ -106,13 +113,59 @@ public class BottomTabsController extends ParentController<BottomTabsLayout> imp
106
113
  bottomTabs.setOnTabSelectedListener(this);
107
114
  root.addBottomTabsContainer(bottomTabsContainer);
108
115
 
116
+ useCustomItemViews = resolveUseCustomItemViews();
117
+ tabPresenter.setUseCustomItemViews(useCustomItemViews);
118
+
109
119
  bottomTabs.addItems(createTabs());
120
+
121
+ if (useCustomItemViews) {
122
+ attachCustomItemViewsToCells();
123
+ }
124
+
110
125
  setInitialTab(resolveCurrentOptions);
111
126
  tabsAttacher.attach();
112
127
 
113
128
  return root;
114
129
  }
115
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
+
116
169
  private void setInitialTab(Options resolveCurrentOptions) {
117
170
  int initialTabIndex = 0;
118
171
  if (resolveCurrentOptions.bottomTabsOptions.currentTabId.hasValue())
@@ -121,6 +174,9 @@ public class BottomTabsController extends ParentController<BottomTabsLayout> imp
121
174
  initialTabIndex = resolveCurrentOptions.bottomTabsOptions.currentTabIndex.get();
122
175
  }
123
176
  bottomTabs.setCurrentItem(initialTabIndex, false);
177
+ if (useCustomItemViews) {
178
+ bottomTabs.onCustomItemViewSelectionChanged(initialTabIndex);
179
+ }
124
180
  }
125
181
 
126
182
  @NonNull
@@ -294,6 +350,9 @@ public class BottomTabsController extends ParentController<BottomTabsLayout> imp
294
350
  ViewController<?> previouslyVisible = getCurrentChild();
295
351
  bottomTabs.setCurrentItem(newIndex, false);
296
352
  getCurrentChild().onSelected(previouslyVisible);
353
+ if (useCustomItemViews) {
354
+ bottomTabs.onCustomItemViewSelectionChanged(newIndex);
355
+ }
297
356
  }
298
357
 
299
358
  private void saveTabSelection(int newIndex, boolean enableSelectionHistory) {
@@ -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
+ }