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.
- package/android/src/main/java/com/reactnativenavigation/NavigationActivity.java +14 -1
- package/android/src/main/java/com/reactnativenavigation/NavigationApplication.java +3 -0
- package/android/src/main/java/com/reactnativenavigation/NavigationPackage.kt +27 -8
- package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRow.kt +262 -0
- package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowAttacher.kt +205 -0
- package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowConfigStore.kt +32 -0
- package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowLayout.kt +139 -0
- package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowModule.kt +37 -0
- package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowOptions.kt +68 -0
- package/android/src/main/java/com/reactnativenavigation/options/BottomTabOptions.java +4 -1
- package/android/src/main/java/com/reactnativenavigation/options/NavigationBarOptions.java +19 -1
- package/android/src/main/java/com/reactnativenavigation/react/ReactView.java +13 -0
- package/android/src/main/java/com/reactnativenavigation/react/events/ComponentType.java +2 -1
- package/android/src/main/java/com/reactnativenavigation/utils/SystemUiUtils.kt +63 -9
- package/android/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabPresenter.java +28 -0
- package/android/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsController.java +77 -6
- package/android/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsPresenter.kt +1 -0
- package/android/src/main/java/com/reactnativenavigation/viewcontrollers/component/ComponentViewController.java +2 -5
- package/android/src/main/java/com/reactnativenavigation/viewcontrollers/viewcontroller/Presenter.java +33 -13
- package/android/src/main/java/com/reactnativenavigation/views/bottomtabs/BottomTabs.java +76 -0
- package/android/src/main/java/com/reactnativenavigation/views/bottomtabs/CustomBottomTabItemView.kt +73 -0
- package/android/src/test/java/com/reactnativenavigation/presentation/PresenterTest.java +14 -0
- package/android/src/test/java/com/reactnativenavigation/utils/SystemUiUtilsTest.kt +64 -1
- package/ios/ARCHITECTURE.md +5 -0
- package/ios/BottomTabPresenter.h +7 -0
- package/ios/BottomTabPresenter.mm +27 -0
- package/ios/RNNAppDelegate.h +16 -0
- package/ios/RNNAppDelegate.mm +73 -0
- package/ios/RNNBottomTabOptions.h +2 -0
- package/ios/RNNBottomTabOptions.mm +5 -1
- package/ios/RNNBottomTabsController.h +2 -0
- package/ios/RNNBottomTabsController.mm +209 -1
- package/ios/RNNBottomTabsCustomRow.h +57 -0
- package/ios/RNNBottomTabsCustomRow.mm +252 -0
- package/ios/RNNBottomTabsCustomRowOptions.h +42 -0
- package/ios/RNNBottomTabsCustomRowOptions.mm +37 -0
- package/ios/RNNBottomTabsOptions.h +2 -0
- package/ios/RNNBottomTabsOptions.mm +2 -0
- package/ios/RNNComponentViewCreator.h +2 -1
- package/ios/RNNCustomTabBarItemView.h +26 -0
- package/ios/RNNCustomTabBarItemView.mm +83 -0
- package/ios/RNNReactRootViewCreator.mm +1 -0
- package/ios/RNNViewControllerFactory.mm +1 -0
- package/ios/ReactNativeNavigation.xcodeproj/project.pbxproj +24 -0
- package/lib/module/ARCHITECTURE.md +30 -0
- package/lib/module/Navigation.js +34 -1
- package/lib/module/Navigation.js.map +1 -1
- package/lib/module/NavigationDelegate.js +21 -0
- package/lib/module/NavigationDelegate.js.map +1 -1
- package/lib/module/adapters/AndroidCustomRowForwarder.js +75 -0
- package/lib/module/adapters/AndroidCustomRowForwarder.js.map +1 -0
- package/lib/module/commands/Commands.js +8 -0
- package/lib/module/commands/Commands.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/interfaces/Options.js.map +1 -1
- package/lib/module/linking/DeferredLinkQueue.js +52 -0
- package/lib/module/linking/DeferredLinkQueue.js.map +1 -0
- package/lib/module/linking/DeferredLinkQueue.test.js +54 -0
- package/lib/module/linking/DeferredLinkQueue.test.js.map +1 -0
- package/lib/module/linking/LinkingHandler.js +139 -0
- package/lib/module/linking/LinkingHandler.js.map +1 -0
- package/lib/module/linking/LinkingHandler.test.js +384 -0
- package/lib/module/linking/LinkingHandler.test.js.map +1 -0
- package/lib/module/linking/ModalLayoutBuilder.js +56 -0
- package/lib/module/linking/ModalLayoutBuilder.js.map +1 -0
- package/lib/module/linking/ModalLayoutBuilder.test.js +154 -0
- package/lib/module/linking/ModalLayoutBuilder.test.js.map +1 -0
- package/lib/module/linking/RouteMatcher.js +104 -0
- package/lib/module/linking/RouteMatcher.js.map +1 -0
- package/lib/module/linking/RouteMatcher.test.js +164 -0
- package/lib/module/linking/RouteMatcher.test.js.map +1 -0
- package/lib/module/linking/URLParser.js +56 -0
- package/lib/module/linking/URLParser.js.map +1 -0
- package/lib/module/linking/URLParser.test.js +100 -0
- package/lib/module/linking/URLParser.test.js.map +1 -0
- package/lib/module/linking/types.js +4 -0
- package/lib/module/linking/types.js.map +1 -0
- package/lib/typescript/Navigation.d.ts +22 -0
- package/lib/typescript/Navigation.d.ts.map +1 -1
- package/lib/typescript/NavigationDelegate.d.ts +13 -0
- package/lib/typescript/NavigationDelegate.d.ts.map +1 -1
- package/lib/typescript/adapters/AndroidCustomRowForwarder.d.ts +23 -0
- package/lib/typescript/adapters/AndroidCustomRowForwarder.d.ts.map +1 -0
- package/lib/typescript/commands/Commands.d.ts +1 -0
- package/lib/typescript/commands/Commands.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +1 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/interfaces/Options.d.ts +90 -0
- package/lib/typescript/interfaces/Options.d.ts.map +1 -1
- package/lib/typescript/linking/DeferredLinkQueue.d.ts +26 -0
- package/lib/typescript/linking/DeferredLinkQueue.d.ts.map +1 -0
- package/lib/typescript/linking/DeferredLinkQueue.test.d.ts +2 -0
- package/lib/typescript/linking/DeferredLinkQueue.test.d.ts.map +1 -0
- package/lib/typescript/linking/LinkingHandler.d.ts +71 -0
- package/lib/typescript/linking/LinkingHandler.d.ts.map +1 -0
- package/lib/typescript/linking/LinkingHandler.test.d.ts +2 -0
- package/lib/typescript/linking/LinkingHandler.test.d.ts.map +1 -0
- package/lib/typescript/linking/ModalLayoutBuilder.d.ts +21 -0
- package/lib/typescript/linking/ModalLayoutBuilder.d.ts.map +1 -0
- package/lib/typescript/linking/ModalLayoutBuilder.test.d.ts +2 -0
- package/lib/typescript/linking/ModalLayoutBuilder.test.d.ts.map +1 -0
- package/lib/typescript/linking/RouteMatcher.d.ts +23 -0
- package/lib/typescript/linking/RouteMatcher.d.ts.map +1 -0
- package/lib/typescript/linking/RouteMatcher.test.d.ts +2 -0
- package/lib/typescript/linking/RouteMatcher.test.d.ts.map +1 -0
- package/lib/typescript/linking/URLParser.d.ts +16 -0
- package/lib/typescript/linking/URLParser.d.ts.map +1 -0
- package/lib/typescript/linking/URLParser.test.d.ts +2 -0
- package/lib/typescript/linking/URLParser.test.d.ts.map +1 -0
- package/lib/typescript/linking/types.d.ts +107 -0
- package/lib/typescript/linking/types.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/ARCHITECTURE.md +30 -0
- package/src/Navigation.ts +36 -1
- package/src/NavigationDelegate.ts +22 -0
- package/src/adapters/AndroidCustomRowForwarder.ts +83 -0
- package/src/commands/Commands.ts +15 -0
- package/src/index.ts +1 -0
- package/src/interfaces/Options.ts +92 -0
- package/src/linking/DeferredLinkQueue.test.ts +60 -0
- package/src/linking/DeferredLinkQueue.ts +55 -0
- package/src/linking/LinkingHandler.test.ts +332 -0
- package/src/linking/LinkingHandler.ts +169 -0
- package/src/linking/ModalLayoutBuilder.test.ts +105 -0
- package/src/linking/ModalLayoutBuilder.ts +60 -0
- package/src/linking/RouteMatcher.test.ts +128 -0
- package/src/linking/RouteMatcher.ts +126 -0
- package/src/linking/URLParser.test.ts +105 -0
- package/src/linking/URLParser.ts +62 -0
- package/src/linking/types.ts +115 -0
|
@@ -5,6 +5,7 @@ import android.content.Intent;
|
|
|
5
5
|
import android.content.res.Configuration;
|
|
6
6
|
import android.os.Build;
|
|
7
7
|
import android.os.Bundle;
|
|
8
|
+
import android.graphics.Color;
|
|
8
9
|
import android.view.KeyEvent;
|
|
9
10
|
import android.view.View;
|
|
10
11
|
import android.view.ViewGroup;
|
|
@@ -25,9 +26,11 @@ import com.reactnativenavigation.viewcontrollers.navigator.Navigator;
|
|
|
25
26
|
|
|
26
27
|
import androidx.activity.EdgeToEdge;
|
|
27
28
|
import androidx.activity.OnBackPressedCallback;
|
|
29
|
+
import androidx.activity.SystemBarStyle;
|
|
28
30
|
import androidx.annotation.NonNull;
|
|
29
31
|
import androidx.annotation.Nullable;
|
|
30
32
|
import androidx.appcompat.app.AppCompatActivity;
|
|
33
|
+
import androidx.core.view.WindowCompat;
|
|
31
34
|
|
|
32
35
|
public class NavigationActivity extends AppCompatActivity implements DefaultHardwareBackBtnHandler, PermissionAwareActivity, JsDevReloadHandler.ReloadListener {
|
|
33
36
|
@Nullable
|
|
@@ -189,8 +192,18 @@ public class NavigationActivity extends AppCompatActivity implements DefaultHard
|
|
|
189
192
|
* calling {@code EdgeToEdge.enable()} directly.
|
|
190
193
|
*/
|
|
191
194
|
protected void activateEdgeToEdge() {
|
|
192
|
-
EdgeToEdge.enable(
|
|
195
|
+
EdgeToEdge.enable(
|
|
196
|
+
this,
|
|
197
|
+
SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT),
|
|
198
|
+
SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT)
|
|
199
|
+
);
|
|
193
200
|
SystemUiUtils.activateEdgeToEdge();
|
|
201
|
+
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
|
202
|
+
SystemUiUtils.setNavigationBarContrastEnforced(getWindow(), false);
|
|
203
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
204
|
+
getWindow().setStatusBarColor(Color.TRANSPARENT);
|
|
205
|
+
getWindow().setNavigationBarColor(Color.TRANSPARENT);
|
|
206
|
+
}
|
|
194
207
|
}
|
|
195
208
|
|
|
196
209
|
protected void addDefaultSplashLayout() {
|
|
@@ -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(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
+
}
|
package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowAttacher.kt
ADDED
|
@@ -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
|
+
}
|
package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowConfigStore.kt
ADDED
|
@@ -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
|
+
}
|