react-native-navigation 8.8.6 → 8.8.7

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 +27 -11
  17. package/android/src/test/java/com/reactnativenavigation/views/TitleAndButtonsContainerTest.kt +15 -1
  18. package/android/src/test/java/com/reactnativenavigation/views/TitleBarReactButtonViewTest.java +135 -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
@@ -7,6 +7,7 @@ import com.facebook.react.ReactNativeHost;
7
7
  import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
8
8
  import com.facebook.react.soloader.OpenSourceMergedSoMapping;
9
9
  import com.facebook.soloader.SoLoader;
10
+ import com.reactnativenavigation.customrow.BottomTabsCustomRowAttacher;
10
11
  import com.reactnativenavigation.react.ReactGateway;
11
12
  import com.reactnativenavigation.viewcontrollers.externalcomponent.ExternalComponentCreator;
12
13
 
@@ -45,6 +46,8 @@ public abstract class NavigationApplication extends Application implements React
45
46
  DefaultNewArchitectureEntryPoint.load();
46
47
 
47
48
  reactGateway = createReactGateway();
49
+
50
+ BottomTabsCustomRowAttacher.INSTANCE.registerOnce(this, null);
48
51
  }
49
52
 
50
53
  /**
@@ -7,6 +7,9 @@ import com.facebook.react.bridge.ReactApplicationContext
7
7
  import com.facebook.react.module.model.ReactModuleInfo
8
8
  import com.facebook.react.module.model.ReactModuleInfoProvider
9
9
  import com.facebook.react.uimanager.ViewManager
10
+ import android.app.Application
11
+ import com.reactnativenavigation.customrow.BottomTabsCustomRowAttacher
12
+ import com.reactnativenavigation.customrow.BottomTabsCustomRowModule
10
13
  import com.reactnativenavigation.options.LayoutFactory
11
14
  import com.reactnativenavigation.react.NavigationTurboModule
12
15
  import com.reactnativenavigation.react.modal.ModalViewManager
@@ -15,10 +18,16 @@ class NavigationPackage() : BaseReactPackage() {
15
18
 
16
19
  override fun getModule(name: String, context: ReactApplicationContext): NativeModule? {
17
20
  val reactApp = context.applicationContext as ReactApplication
21
+ (context.applicationContext as? Application)?.let {
22
+ BottomTabsCustomRowAttacher.registerOnce(it, context.currentActivity)
23
+ }
18
24
  return when (name) {
19
25
  NavigationTurboModule.NAME -> {
20
26
  NavigationTurboModule(context, LayoutFactory(reactApp.reactHost))
21
27
  }
28
+ BottomTabsCustomRowModule.NAME -> {
29
+ BottomTabsCustomRowModule(context)
30
+ }
22
31
  else -> {
23
32
  null
24
33
  }
@@ -26,14 +35,24 @@ class NavigationPackage() : BaseReactPackage() {
26
35
  }
27
36
 
28
37
  override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
29
- mapOf(NavigationTurboModule.NAME to ReactModuleInfo(
30
- name = NavigationTurboModule.NAME,
31
- className = NavigationTurboModule.NAME,
32
- canOverrideExistingModule = false,
33
- needsEagerInit = false,
34
- isCxxModule = false,
35
- isTurboModule = true
36
- ))
38
+ mapOf(
39
+ NavigationTurboModule.NAME to ReactModuleInfo(
40
+ name = NavigationTurboModule.NAME,
41
+ className = NavigationTurboModule.NAME,
42
+ canOverrideExistingModule = false,
43
+ needsEagerInit = false,
44
+ isCxxModule = false,
45
+ isTurboModule = true
46
+ ),
47
+ BottomTabsCustomRowModule.NAME to ReactModuleInfo(
48
+ name = BottomTabsCustomRowModule.NAME,
49
+ className = BottomTabsCustomRowModule.NAME,
50
+ canOverrideExistingModule = false,
51
+ needsEagerInit = true,
52
+ isCxxModule = false,
53
+ isTurboModule = false
54
+ )
55
+ )
37
56
  }
38
57
 
39
58
  override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
@@ -0,0 +1,262 @@
1
+ package com.reactnativenavigation.customrow
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.content.Context
5
+ import android.graphics.Color
6
+ import android.graphics.Outline
7
+ import android.graphics.RenderEffect
8
+ import android.graphics.Shader
9
+ import android.os.Build
10
+ import android.util.Log
11
+ import android.util.TypedValue
12
+ import android.view.MotionEvent
13
+ import android.view.View
14
+ import android.view.ViewGroup
15
+ import android.view.ViewOutlineProvider
16
+ import android.widget.FrameLayout
17
+ import com.reactnativenavigation.views.bottomtabs.BottomTabs
18
+ import com.reactnativenavigation.views.bottomtabs.CustomBottomTabItemView
19
+
20
+ /**
21
+ * Floating row that hosts the React-rendered `CustomBottomTabItemView`
22
+ * cells produced by RNN's existing custom-tab path. Mimics the iOS
23
+ * `RNNBottomTabsCustomRow` (Approach B): the underlying `BottomTabs` view
24
+ * is kept for state but its visuals are hidden; this view is the only
25
+ * thing the user sees.
26
+ *
27
+ * Strict zero-touch on the existing tabs implementation: only public APIs
28
+ * of `BottomTabs` (`hasCustomItemViews`, `getCustomItemView`,
29
+ * `getItemsCount`, `setCurrentItem`) are used, plus standard `View`
30
+ * methods.
31
+ */
32
+ @SuppressLint("ViewConstructor")
33
+ class BottomTabsCustomRow(
34
+ context: Context,
35
+ private val bottomTabs: BottomTabs,
36
+ ) : FrameLayout(context) {
37
+
38
+ private val cells = mutableListOf<Cell>()
39
+ private var currentOptions: BottomTabsCustomRowOptions = BottomTabsCustomRowConfigStore.get()
40
+ private var selectedIndex: Int = bottomTabs.currentItem
41
+ private var safeBottomInsetPx: Int = 0
42
+
43
+ /**
44
+ * Visible chrome (background colour, rounded corners, shadow) that also
45
+ * hosts the cell views as children. Lives as a dedicated child of the
46
+ * row so it can be inset from the row's bottom by
47
+ * `safeBottom + bottomMargin` and only paint over the content area —
48
+ * mirrors iOS's `backgroundColorView` / `backgroundEffectView`. Hosting
49
+ * the cells inside it ensures cells render *above* the chrome regardless
50
+ * of elevation.
51
+ */
52
+ private val backgroundView: FrameLayout = FrameLayout(context).apply {
53
+ clipToOutline = true
54
+ clipChildren = true
55
+ outlineProvider = object : ViewOutlineProvider() {
56
+ override fun getOutline(view: View, outline: Outline) {
57
+ val r = effectiveCornerRadiusPx()
58
+ outline.setRoundRect(0, 0, view.width, view.height, r)
59
+ }
60
+ }
61
+ }
62
+
63
+ private val configListener: (BottomTabsCustomRowOptions) -> Unit = { opts ->
64
+ post { applyOptions(opts) }
65
+ }
66
+
67
+ init {
68
+ setWillNotDraw(true)
69
+ clipChildren = false
70
+ clipToPadding = false
71
+ addView(backgroundView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
72
+ BottomTabsCustomRowConfigStore.addListener(configListener)
73
+ applyOptions(currentOptions)
74
+ rebuildCells()
75
+ }
76
+
77
+ override fun onDetachedFromWindow() {
78
+ super.onDetachedFromWindow()
79
+ BottomTabsCustomRowConfigStore.removeListener(configListener)
80
+ }
81
+
82
+ fun rebuildCells() {
83
+ for (cell in cells) {
84
+ (cell.parent as? ViewGroup)?.removeView(cell)
85
+ }
86
+ cells.clear()
87
+ if (!bottomTabs.hasCustomItemViews()) return
88
+
89
+ val count = bottomTabs.itemsCount
90
+ for (i in 0 until count) {
91
+ val itemView = bottomTabs.getCustomItemView(i) ?: continue
92
+ (itemView.parent as? ViewGroup)?.removeView(itemView)
93
+ val cell = Cell(context, i, itemView).also {
94
+ it.setOnClickListener { _ ->
95
+ bottomTabs.setCurrentItem(i, true)
96
+ setSelectedIndex(i)
97
+ }
98
+ }
99
+ backgroundView.addView(
100
+ cell,
101
+ FrameLayout.LayoutParams(
102
+ FrameLayout.LayoutParams.MATCH_PARENT,
103
+ FrameLayout.LayoutParams.MATCH_PARENT
104
+ )
105
+ )
106
+ cells.add(cell)
107
+ }
108
+ setSelectedIndex(selectedIndex)
109
+ requestLayout()
110
+ }
111
+
112
+ fun setSelectedIndex(index: Int) {
113
+ selectedIndex = index
114
+ for (cell in cells) cell.itemView.setItemSelected(cell.index == index)
115
+ }
116
+
117
+ fun applyOptions(options: BottomTabsCustomRowOptions) {
118
+ currentOptions = options
119
+
120
+ val solidColor = options.backgroundColor
121
+ val effect = options.backgroundEffect
122
+ val backgroundColorToUse = solidColor
123
+ ?: if (effect == BottomTabsCustomRowOptions.BackgroundEffect.None)
124
+ Color.TRANSPARENT
125
+ else
126
+ materialChromeColor()
127
+
128
+ backgroundView.setBackgroundColor(backgroundColorToUse)
129
+
130
+ // NOTE: Android does not expose a true blur-behind API for arbitrary
131
+ // views (the `RenderEffect.createBlurEffect` API blurs the view's own
132
+ // rendered content, which would smear the cells we host). For
133
+ // `glass` / `blur` we therefore render an opaque-ish chrome material
134
+ // colour; a future enhancement can swap this for `eightbitlab/
135
+ // BlurView` to get true blur-behind on Android.
136
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
137
+ backgroundView.setRenderEffect(null)
138
+ }
139
+
140
+ // iOS gets a soft visual lift from `UIGlassEffect`'s built-in ring +
141
+ // highlight. Android has no equivalent, so we emulate it with a
142
+ // low-elevation shadow tinted at low alpha. Hard `elevation = 8` (the
143
+ // Material default) looks much heavier than the iOS reference, so
144
+ // we stay subtle: ~3dp elevation, ~30% / 12% shadow alpha.
145
+ backgroundView.elevation = dp(3f)
146
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
147
+ backgroundView.outlineSpotShadowColor = Color.argb(0x4C, 0, 0, 0)
148
+ backgroundView.outlineAmbientShadowColor = Color.argb(0x1E, 0, 0, 0)
149
+ }
150
+ backgroundView.invalidateOutline()
151
+ requestLayout()
152
+ }
153
+
154
+ private fun materialChromeColor(): Int {
155
+ val typed = TypedValue()
156
+ val resolved = context.theme.resolveAttribute(
157
+ android.R.attr.colorBackground, typed, true
158
+ )
159
+ val base = if (resolved && typed.type >= TypedValue.TYPE_FIRST_COLOR_INT &&
160
+ typed.type <= TypedValue.TYPE_LAST_COLOR_INT
161
+ ) typed.data else Color.WHITE
162
+ return alphaOver(base, 0xF0)
163
+ }
164
+
165
+ private fun alphaOver(color: Int, alpha: Int): Int {
166
+ return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color))
167
+ }
168
+
169
+ private fun effectiveCornerRadiusPx(): Float {
170
+ val dp = currentOptions.cornerRadius ?: 28f
171
+ return dp(dp)
172
+ }
173
+
174
+ /**
175
+ * Content-area height (where cells live), excluding the bottom safe-area
176
+ * inset and bottomMargin.
177
+ */
178
+ fun effectiveContentHeightPx(nativeBarHeightPx: Int): Int {
179
+ val heightOption = currentOptions.height
180
+ if (heightOption != null) {
181
+ val configured = dp(heightOption).toInt()
182
+ // JS height targets iOS floating chrome; keep Android shell tight so
183
+ // the pill sits on the tab bar without a tall empty box above nav.
184
+ return minOf(configured, nativeBarHeightPx.coerceAtLeast(dp(52f).toInt()))
185
+ }
186
+ // Default: a touch taller than the native bar's content area to give
187
+ // icon + label + pill enough room — mirrors iOS's +18pt default.
188
+ val nativeContent = (nativeBarHeightPx - safeBottomInsetPx).coerceAtLeast(0)
189
+ return (nativeContent + dp(18f)).toInt()
190
+ }
191
+
192
+ /**
193
+ * Total row height including the bottom safe-area inset and bottomMargin
194
+ * — mirrors iOS's `desiredRowHeightForNativeTabBarHeight:safeBottom:`.
195
+ */
196
+ fun effectiveTotalHeightPx(nativeBarHeightPx: Int): Int =
197
+ effectiveContentHeightPx(nativeBarHeightPx) + safeBottomInsetPx + effectiveBottomMarginPx()
198
+
199
+ fun effectiveHorizontalMarginPx(): Int = dp(currentOptions.horizontalMargin ?: 16f).toInt()
200
+ fun effectiveBottomMarginPx(): Int = dp(currentOptions.bottomMargin ?: 0f).toInt()
201
+
202
+ fun setSafeBottomInsetPx(insetPx: Int) {
203
+ if (safeBottomInsetPx == insetPx) return
204
+ safeBottomInsetPx = insetPx
205
+ requestLayout()
206
+ }
207
+
208
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
209
+ super.onLayout(changed, left, top, right, bottom)
210
+ val w = width
211
+ // Visible chrome only covers the content area — never the safe-area
212
+ // strip below (matches iOS where `backgroundEffectView.frame =
213
+ // content` excludes `safe.bottom + bottomMargin`).
214
+ val contentBottom = (height - safeBottomInsetPx - effectiveBottomMarginPx()).coerceAtLeast(0)
215
+ val bgSpec = MeasureSpec.makeMeasureSpec(w, MeasureSpec.EXACTLY)
216
+ val bgHeightSpec = MeasureSpec.makeMeasureSpec(contentBottom, MeasureSpec.EXACTLY)
217
+ backgroundView.measure(bgSpec, bgHeightSpec)
218
+ backgroundView.layout(0, 0, w, contentBottom)
219
+ backgroundView.invalidateOutline()
220
+ layoutCells(w, contentBottom)
221
+ }
222
+
223
+ private fun layoutCells(parentWidth: Int, parentHeight: Int) {
224
+ if (cells.isEmpty() || parentWidth <= 0 || parentHeight <= 0) return
225
+ val per = parentWidth.toFloat() / cells.size.toFloat()
226
+ for (i in cells.indices) {
227
+ val cell = cells[i]
228
+ val l = (i * per).toInt()
229
+ val r = ((i + 1) * per).toInt()
230
+ val widthSpec = MeasureSpec.makeMeasureSpec(r - l, MeasureSpec.EXACTLY)
231
+ val heightSpec = MeasureSpec.makeMeasureSpec(parentHeight, MeasureSpec.EXACTLY)
232
+ cell.measure(widthSpec, heightSpec)
233
+ // Cells are children of `backgroundView`, laid out in its local
234
+ // coordinate space (which is already 0..contentBottom).
235
+ cell.layout(l, 0, r, parentHeight)
236
+ }
237
+ }
238
+
239
+ private fun dp(value: Float): Float =
240
+ value * resources.displayMetrics.density
241
+
242
+ @SuppressLint("ViewConstructor")
243
+ private class Cell(
244
+ context: Context,
245
+ val index: Int,
246
+ val itemView: CustomBottomTabItemView,
247
+ ) : FrameLayout(context) {
248
+ init {
249
+ isClickable = true
250
+ isFocusable = true
251
+ // Host the React item view inside this cell so it inherits taps
252
+ // we don't consume.
253
+ addView(itemView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
254
+ }
255
+
256
+ override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
257
+ // Bypass the React view's pass-through and let this Cell receive
258
+ // the click event directly.
259
+ return onTouchEvent(ev) || super.dispatchTouchEvent(ev)
260
+ }
261
+ }
262
+ }
@@ -0,0 +1,205 @@
1
+ package com.reactnativenavigation.customrow
2
+
3
+ import android.app.Activity
4
+ import android.app.Application
5
+ import android.os.Build
6
+ import android.os.Bundle
7
+ import android.view.View
8
+ import android.view.ViewGroup
9
+ import android.view.ViewTreeObserver
10
+ import android.view.WindowInsets
11
+ import android.widget.FrameLayout
12
+ import com.reactnativenavigation.views.bottomtabs.BottomTabs
13
+
14
+ /**
15
+ * Activity-lifecycle observer that watches every started activity for
16
+ * `BottomTabs` instances using the existing custom-tab path and injects a
17
+ * [BottomTabsCustomRow] above them, hiding the native chrome via public
18
+ * `View` APIs (`alpha = 0f`).
19
+ *
20
+ * Layout listeners are registered once per activity (on the decor view) and
21
+ * placement updates are deduplicated so Espresso / Detox can reach idle.
22
+ */
23
+ internal object BottomTabsCustomRowAttacher : Application.ActivityLifecycleCallbacks {
24
+
25
+ @Volatile private var registered: Boolean = false
26
+ @Volatile private var lastResumedActivity: Activity? = null
27
+
28
+ private data class LastPlacement(
29
+ val left: Int,
30
+ val top: Int,
31
+ val width: Int,
32
+ val height: Int,
33
+ val safeBottomInsetPx: Int,
34
+ )
35
+
36
+ fun registerOnce(application: Application, currentActivity: Activity? = null) {
37
+ if (!registered) {
38
+ registered = true
39
+ application.registerActivityLifecycleCallbacks(this)
40
+ }
41
+ if (currentActivity != null && lastResumedActivity == null) {
42
+ lastResumedActivity = currentActivity
43
+ ensureLayoutObserver(currentActivity)
44
+ tryAttach(currentActivity)
45
+ }
46
+ }
47
+
48
+ fun rescan() {
49
+ val activity = lastResumedActivity ?: return
50
+ tryAttach(activity)
51
+ }
52
+
53
+ override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
54
+ ensureLayoutObserver(activity)
55
+ tryAttach(activity)
56
+ }
57
+
58
+ override fun onActivityStarted(activity: Activity) {
59
+ ensureLayoutObserver(activity)
60
+ tryAttach(activity)
61
+ }
62
+
63
+ override fun onActivityResumed(activity: Activity) {
64
+ lastResumedActivity = activity
65
+ ensureLayoutObserver(activity)
66
+ tryAttach(activity)
67
+ }
68
+
69
+ override fun onActivityPaused(activity: Activity) {
70
+ if (lastResumedActivity === activity) lastResumedActivity = null
71
+ }
72
+
73
+ override fun onActivityStopped(activity: Activity) {}
74
+ override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
75
+ override fun onActivityDestroyed(activity: Activity) {
76
+ activity.window?.decorView?.setTag(TAG_OBSERVING, null)
77
+ }
78
+
79
+ private fun ensureLayoutObserver(activity: Activity) {
80
+ val decor = activity.window?.decorView as? ViewGroup ?: return
81
+ if (decor.getTag(TAG_OBSERVING) == true) return
82
+ decor.setTag(TAG_OBSERVING, true)
83
+ decor.viewTreeObserver.addOnGlobalLayoutListener(
84
+ object : ViewTreeObserver.OnGlobalLayoutListener {
85
+ override fun onGlobalLayout() {
86
+ tryAttach(activity)
87
+ }
88
+ }
89
+ )
90
+ }
91
+
92
+ private fun tryAttach(activity: Activity) {
93
+ val scanRoot = activity.window?.decorView as? ViewGroup
94
+ ?: activity.findViewById<View>(android.R.id.content) as? ViewGroup
95
+ ?: return
96
+ val overlayHost = activity.findViewById<View>(android.R.id.content) as? ViewGroup
97
+ ?: scanRoot
98
+
99
+ forEachBottomTabs(scanRoot) { bottomTabs ->
100
+ if (!bottomTabs.hasCustomItemViews()) return@forEachBottomTabs
101
+
102
+ val existing = bottomTabs.getTag(TAG_ATTACHED_ROW_ID) as? BottomTabsCustomRow
103
+ if (existing != null) {
104
+ ensureRowHostedOn(existing, overlayHost)
105
+ positionRow(existing, bottomTabs, overlayHost, activity)
106
+ return@forEachBottomTabs
107
+ }
108
+
109
+ bottomTabs.setExternalCustomItemViewHost(true)
110
+ val row = BottomTabsCustomRow(overlayHost.context, bottomTabs)
111
+ overlayHost.addView(
112
+ row,
113
+ FrameLayout.LayoutParams(
114
+ FrameLayout.LayoutParams.WRAP_CONTENT,
115
+ FrameLayout.LayoutParams.WRAP_CONTENT
116
+ )
117
+ )
118
+ bottomTabs.setTag(TAG_ATTACHED_ROW_ID, row)
119
+ bottomTabs.alpha = 0f
120
+ bottomTabs.elevation = 0f
121
+ positionRow(row, bottomTabs, overlayHost, activity)
122
+ }
123
+ }
124
+
125
+ private fun ensureRowHostedOn(row: BottomTabsCustomRow, overlayHost: ViewGroup) {
126
+ if (row.parent === overlayHost) return
127
+ (row.parent as? ViewGroup)?.removeView(row)
128
+ overlayHost.addView(
129
+ row,
130
+ FrameLayout.LayoutParams(
131
+ FrameLayout.LayoutParams.WRAP_CONTENT,
132
+ FrameLayout.LayoutParams.WRAP_CONTENT
133
+ )
134
+ )
135
+ }
136
+
137
+ private fun positionRow(
138
+ row: BottomTabsCustomRow,
139
+ bottomTabs: BottomTabs,
140
+ overlayHost: ViewGroup,
141
+ activity: Activity,
142
+ ) {
143
+ val navBarInsetPx = systemBottomInsetPx(overlayHost, bottomTabs)
144
+ val placement = BottomTabsCustomRowLayout.resolvePlacement(
145
+ activity,
146
+ row,
147
+ bottomTabs,
148
+ overlayHost,
149
+ navBarInsetPx,
150
+ ) ?: return
151
+
152
+ val next = LastPlacement(
153
+ placement.left,
154
+ placement.top,
155
+ placement.width,
156
+ placement.height,
157
+ placement.rowSafeBottomInsetPx,
158
+ )
159
+ if (row.getTag(TAG_LAST_PLACEMENT) == next) {
160
+ return
161
+ }
162
+ row.setTag(TAG_LAST_PLACEMENT, next)
163
+ row.setSafeBottomInsetPx(placement.rowSafeBottomInsetPx)
164
+
165
+ val lp = (row.layoutParams as? FrameLayout.LayoutParams)
166
+ ?: FrameLayout.LayoutParams(placement.width, placement.height)
167
+ lp.width = placement.width
168
+ lp.height = placement.height
169
+ lp.leftMargin = placement.left
170
+ lp.topMargin = placement.top
171
+ row.layoutParams = lp
172
+ row.bringToFront()
173
+ }
174
+
175
+ private fun systemBottomInsetPx(overlayHost: View, bottomTabs: View): Int {
176
+ for (source in listOf(bottomTabs, overlayHost)) {
177
+ val insets = source.rootWindowInsets ?: continue
178
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
179
+ val navBars = insets.getInsets(WindowInsets.Type.navigationBars()).bottom
180
+ if (navBars > 0) return navBars
181
+ } else {
182
+ @Suppress("DEPRECATION")
183
+ val legacy = insets.systemWindowInsetBottom
184
+ if (legacy > 0) return legacy
185
+ }
186
+ }
187
+ return 0
188
+ }
189
+
190
+ private fun forEachBottomTabs(view: View, block: (BottomTabs) -> Unit) {
191
+ if (view is BottomTabs) {
192
+ block(view)
193
+ return
194
+ }
195
+ if (view is ViewGroup) {
196
+ for (i in 0 until view.childCount) {
197
+ forEachBottomTabs(view.getChildAt(i), block)
198
+ }
199
+ }
200
+ }
201
+
202
+ private val TAG_ATTACHED_ROW_ID = "rnnBottomTabsCustomRow".hashCode()
203
+ private val TAG_OBSERVING = "rnnCustomRowObserving".hashCode()
204
+ private val TAG_LAST_PLACEMENT = "rnnCustomRowLastPlacement".hashCode()
205
+ }
@@ -0,0 +1,32 @@
1
+ package com.reactnativenavigation.customrow
2
+
3
+ /**
4
+ * Process-wide singleton holding the latest custom-row configuration
5
+ * pushed from JS via the `RNNBottomTabsCustomRowModule` native module.
6
+ *
7
+ * The attacher reads from here when it needs to apply chrome to a freshly
8
+ * detected `BottomTabs` instance.
9
+ */
10
+ object BottomTabsCustomRowConfigStore {
11
+ @Volatile
12
+ private var current: BottomTabsCustomRowOptions = BottomTabsCustomRowOptions()
13
+
14
+ private val listeners = mutableSetOf<(BottomTabsCustomRowOptions) -> Unit>()
15
+
16
+ fun update(options: BottomTabsCustomRowOptions) {
17
+ current = options
18
+ synchronized(listeners) {
19
+ listeners.toList().forEach { it.invoke(options) }
20
+ }
21
+ }
22
+
23
+ fun get(): BottomTabsCustomRowOptions = current
24
+
25
+ fun addListener(listener: (BottomTabsCustomRowOptions) -> Unit) {
26
+ synchronized(listeners) { listeners.add(listener) }
27
+ }
28
+
29
+ fun removeListener(listener: (BottomTabsCustomRowOptions) -> Unit) {
30
+ synchronized(listeners) { listeners.remove(listener) }
31
+ }
32
+ }