react-native-screens 4.25.0-beta.1 → 4.25.0-beta.3
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/swmansion/rnscreens/gamma/stack/header/StackHeaderAppBarLayout.kt +6 -14
- package/android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/StackHeaderAppBarLayoutBehavior.kt +29 -0
- package/android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/StackHeaderCoordinator.kt +56 -0
- package/android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/config/StackHeaderConfig.kt +11 -0
- package/android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/config/StackHeaderConfigProviding.kt +5 -0
- package/android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/config/StackHeaderConfigViewManager.kt +35 -0
- package/android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/subview/StackHeaderSubview.kt +3 -7
- package/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/TabsActionOrigin.kt +26 -0
- package/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/TabsContainer.kt +227 -151
- package/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/TabsNavigationState.kt +60 -0
- package/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/{TabsContainerDelegate.kt → TabsNavigationStateObserver.kt} +19 -14
- package/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/TabsNavigationStateObserverRegistry.kt +88 -0
- package/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/TabsHost.kt +40 -24
- package/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/TabsHostEventEmitter.kt +11 -9
- package/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/TabsHostViewManager.kt +19 -7
- package/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/event/TabsHostTabSelectedEvent.kt +4 -3
- package/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/event/TabsHostTabSelectionPreventedEvent.kt +3 -3
- package/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/event/TabsHostTabSelectionRejectedEvent.kt +12 -11
- package/ios/conversion/RNSConversions-Tabs.mm +19 -0
- package/ios/conversion/RNSConversions.h +3 -0
- package/ios/tabs/bottom-accessory/RNSTabsBottomAccessoryHelper.mm +34 -5
- package/ios/tabs/host/RNSTabBarController.h +152 -99
- package/ios/tabs/host/RNSTabBarController.mm +137 -113
- package/ios/tabs/host/RNSTabsHostComponentView.h +7 -8
- package/ios/tabs/host/RNSTabsHostComponentView.mm +37 -33
- package/ios/tabs/host/RNSTabsHostEventEmitter.h +4 -4
- package/ios/tabs/host/RNSTabsHostEventEmitter.mm +5 -3
- package/ios/tabs/host/RNSTabsNavigationState.h +142 -27
- package/ios/tabs/host/RNSTabsNavigationState.mm +35 -2
- package/ios/tabs/host/RNSTabsNavigationStateObserverRegistry.h +62 -0
- package/ios/tabs/host/RNSTabsNavigationStateObserverRegistry.mm +104 -0
- package/lib/commonjs/components/gamma/stack/header/StackHeaderConfig.android.js +46 -1
- package/lib/commonjs/components/gamma/stack/header/StackHeaderConfig.android.js.map +1 -1
- package/lib/commonjs/components/safe-area/SafeAreaView.web.js +2 -3
- package/lib/commonjs/components/safe-area/SafeAreaView.web.js.map +1 -1
- package/lib/commonjs/components/tabs/host/TabsHost.android.js +2 -2
- package/lib/commonjs/components/tabs/host/TabsHost.android.js.map +1 -1
- package/lib/commonjs/components/tabs/host/TabsHost.ios.js +2 -2
- package/lib/commonjs/components/tabs/host/TabsHost.ios.js.map +1 -1
- package/lib/commonjs/fabric/gamma/stack/StackHeaderConfigAndroidNativeComponent.js.map +1 -1
- package/lib/commonjs/flags.js +1 -0
- package/lib/commonjs/flags.js.map +1 -1
- package/lib/module/components/gamma/stack/header/StackHeaderConfig.android.js +46 -1
- package/lib/module/components/gamma/stack/header/StackHeaderConfig.android.js.map +1 -1
- package/lib/module/components/safe-area/SafeAreaView.web.js +1 -1
- package/lib/module/components/safe-area/SafeAreaView.web.js.map +1 -1
- package/lib/module/components/tabs/host/TabsHost.android.js +2 -2
- package/lib/module/components/tabs/host/TabsHost.android.js.map +1 -1
- package/lib/module/components/tabs/host/TabsHost.ios.js +2 -2
- package/lib/module/components/tabs/host/TabsHost.ios.js.map +1 -1
- package/lib/module/fabric/gamma/stack/StackHeaderConfigAndroidNativeComponent.js.map +1 -1
- package/lib/module/flags.js +1 -0
- package/lib/module/flags.js.map +1 -1
- package/lib/typescript/components/gamma/split/SplitHost.types.d.ts +1 -1
- package/lib/typescript/components/gamma/split/SplitHost.types.d.ts.map +1 -1
- package/lib/typescript/components/gamma/stack/header/StackHeaderConfig.android.d.ts.map +1 -1
- package/lib/typescript/components/gamma/stack/header/StackHeaderConfig.android.types.d.ts +183 -8
- package/lib/typescript/components/gamma/stack/header/StackHeaderConfig.android.types.d.ts.map +1 -1
- package/lib/typescript/components/gamma/stack/header/StackHeaderConfig.types.d.ts +37 -0
- package/lib/typescript/components/gamma/stack/header/StackHeaderConfig.types.d.ts.map +1 -1
- package/lib/typescript/components/gamma/stack/header/android/StackHeaderSubview.android.types.d.ts +1 -1
- package/lib/typescript/components/gamma/stack/header/android/StackHeaderSubview.android.types.d.ts.map +1 -1
- package/lib/typescript/components/gamma/stack/host/StackHost.types.d.ts +1 -1
- package/lib/typescript/components/gamma/stack/host/StackHost.types.d.ts.map +1 -1
- package/lib/typescript/components/safe-area/SafeAreaView.web.d.ts +1 -1
- package/lib/typescript/components/safe-area/SafeAreaView.web.d.ts.map +1 -1
- package/lib/typescript/components/tabs/host/TabsHost.types.d.ts +29 -19
- package/lib/typescript/components/tabs/host/TabsHost.types.d.ts.map +1 -1
- package/lib/typescript/components/tabs/index.d.ts +1 -1
- package/lib/typescript/components/tabs/index.d.ts.map +1 -1
- package/lib/typescript/fabric/gamma/stack/StackHeaderConfigAndroidNativeComponent.d.ts +5 -0
- package/lib/typescript/fabric/gamma/stack/StackHeaderConfigAndroidNativeComponent.d.ts.map +1 -1
- package/lib/typescript/fabric/tabs/TabsHostAndroidNativeComponent.d.ts +5 -5
- package/lib/typescript/fabric/tabs/TabsHostAndroidNativeComponent.d.ts.map +1 -1
- package/lib/typescript/fabric/tabs/TabsHostIOSNativeComponent.d.ts +5 -5
- package/lib/typescript/fabric/tabs/TabsHostIOSNativeComponent.d.ts.map +1 -1
- package/lib/typescript/flags.d.ts +1 -0
- package/lib/typescript/flags.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/gamma/split/SplitHost.types.ts +1 -1
- package/src/components/gamma/stack/header/StackHeaderConfig.android.tsx +72 -2
- package/src/components/gamma/stack/header/StackHeaderConfig.android.types.ts +183 -8
- package/src/components/gamma/stack/header/StackHeaderConfig.types.ts +37 -0
- package/src/components/gamma/stack/header/android/StackHeaderSubview.android.types.ts +1 -1
- package/src/components/gamma/stack/host/StackHost.types.ts +1 -1
- package/src/components/safe-area/SafeAreaView.web.tsx +1 -1
- package/src/components/tabs/host/TabsHost.android.tsx +2 -2
- package/src/components/tabs/host/TabsHost.ios.tsx +2 -2
- package/src/components/tabs/host/TabsHost.types.ts +29 -19
- package/src/components/tabs/index.ts +1 -1
- package/src/fabric/gamma/stack/StackHeaderConfigAndroidNativeComponent.ts +6 -0
- package/src/fabric/tabs/TabsHostAndroidNativeComponent.ts +5 -5
- package/src/fabric/tabs/TabsHostIOSNativeComponent.ts +5 -5
- package/src/flags.ts +1 -0
- package/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/TabsContainerOps.kt +0 -7
- package/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/TabsNavState.kt +0 -43
|
@@ -36,10 +36,17 @@ import com.swmansion.rnscreens.safearea.SafeAreaView
|
|
|
36
36
|
import com.swmansion.rnscreens.utils.RNSLog
|
|
37
37
|
import kotlin.properties.Delegates
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* View that hosts the bottom navigation bar and the currently selected tab's content.
|
|
41
|
+
*
|
|
42
|
+
* Public API surface (the only contract third-party native consumers should rely on) is
|
|
43
|
+
* grouped under the `Public API` region below. Members in `Host-internal API` and other
|
|
44
|
+
* regions are implementation detail — the host (`TabsHost`) is the only intended caller
|
|
45
|
+
* and these can change without notice.
|
|
46
|
+
*/
|
|
39
47
|
@SuppressLint("ViewConstructor") // Created only by us. Should never be restored.
|
|
40
|
-
|
|
48
|
+
class TabsContainer internal constructor(
|
|
41
49
|
private val context: Context,
|
|
42
|
-
private val delegate: TabsContainerDelegate,
|
|
43
50
|
) : FrameLayout(context),
|
|
44
51
|
ColorSchemeProviding,
|
|
45
52
|
TabsScreenDelegate,
|
|
@@ -70,23 +77,25 @@ internal class TabsContainer(
|
|
|
70
77
|
}
|
|
71
78
|
}
|
|
72
79
|
|
|
73
|
-
private var navState:
|
|
74
|
-
private var lastUINavState:
|
|
80
|
+
private var navState: TabsNavigationState = TabsNavigationState.EMPTY
|
|
81
|
+
private var lastUINavState: TabsNavigationState = TabsNavigationState.EMPTY
|
|
75
82
|
private val tabsModel: MutableList<TabsScreenFragment> = arrayListOf()
|
|
76
|
-
|
|
83
|
+
|
|
84
|
+
internal var rejectStaleNavigationStateUpdates: Boolean = false
|
|
77
85
|
|
|
78
86
|
internal val selectedTab: TabsScreenFragment
|
|
79
87
|
get() =
|
|
80
|
-
checkNotNull(getFragmentForScreenKey(navState.
|
|
88
|
+
checkNotNull(getFragmentForScreenKey(navState.selectedScreenKey)) { "[RNScreens] No selected tab present" }
|
|
81
89
|
|
|
82
90
|
internal val invalidationFlags = TabsContainerInvalidationFlags()
|
|
83
91
|
|
|
84
|
-
private var
|
|
85
|
-
|
|
86
|
-
|
|
92
|
+
private var pendingStateUpdateRequest: TabsNavigationStateUpdateRequest? = null
|
|
93
|
+
|
|
94
|
+
private fun requirePendingStateUpdateRequest(): TabsNavigationStateUpdateRequest =
|
|
95
|
+
checkNotNull(pendingStateUpdateRequest) { "[RNScreens] Attempt to require nullish pendingStateUpdateRequest" }
|
|
87
96
|
|
|
88
97
|
/**
|
|
89
|
-
* Denotes whether container is currently performing update triggered by the `
|
|
98
|
+
* Denotes whether container is currently performing update triggered by the `pendingStateUpdateRequest`.
|
|
90
99
|
*/
|
|
91
100
|
private var isInExternalOperationContext: Boolean = false
|
|
92
101
|
|
|
@@ -113,6 +122,8 @@ internal class TabsContainer(
|
|
|
113
122
|
private val specialEffectsHandler = SpecialEffectsHandler()
|
|
114
123
|
private val colorSchemeCoordinator = ColorSchemeCoordinator()
|
|
115
124
|
|
|
125
|
+
private val observerRegistry = TabsNavigationStateObserverRegistry()
|
|
126
|
+
|
|
116
127
|
internal var colorScheme: ColorScheme by colorSchemeCoordinator::colorScheme
|
|
117
128
|
internal var tabBarRespectsIMEInsets: Boolean = false
|
|
118
129
|
|
|
@@ -138,9 +149,8 @@ internal class TabsContainer(
|
|
|
138
149
|
updateInterfaceInsets()
|
|
139
150
|
invalidationFlags.isNavigationMenuAppearanceInvalidated = true
|
|
140
151
|
post {
|
|
141
|
-
|
|
152
|
+
flushPendingUpdates()
|
|
142
153
|
}
|
|
143
|
-
// updateNavigationMenuIfNeeded(oldValue, newValue)
|
|
144
154
|
}
|
|
145
155
|
}
|
|
146
156
|
|
|
@@ -152,12 +162,110 @@ internal class TabsContainer(
|
|
|
152
162
|
invalidationFlags.invalidateAll()
|
|
153
163
|
}
|
|
154
164
|
|
|
165
|
+
// region Public API (third-party stable)
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Current navigation state of the container.
|
|
169
|
+
* Returns [TabsNavigationState.EMPTY] before the first tab is selected.
|
|
170
|
+
*/
|
|
171
|
+
val navigationState: TabsNavigationState
|
|
172
|
+
get() = navState
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Queue a navigation state update. Apply via [flushPendingUpdates] or rely on the
|
|
176
|
+
* host's next render cycle to apply automatically.
|
|
177
|
+
*/
|
|
178
|
+
fun submitSelectionOfTabsScreenWithKey(screenKey: String) {
|
|
179
|
+
setPendingNavigationStateUpdate(
|
|
180
|
+
TabsNavigationStateUpdateRequest(
|
|
181
|
+
screenKey,
|
|
182
|
+
navigationState.provenance,
|
|
183
|
+
TabsActionOrigin.PROGRAMMATIC_NATIVE,
|
|
184
|
+
),
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Apply any pending invalidations and state updates in a single coordinated pass.
|
|
190
|
+
* No-op when nothing is dirty or the view is detached.
|
|
191
|
+
*/
|
|
192
|
+
fun flushPendingUpdates() {
|
|
193
|
+
if (invalidationFlags.any() && isAttachedToWindow) {
|
|
194
|
+
performContainerUpdate()
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
fun addNavigationStateObserver(observer: TabsNavigationStateObserver): Boolean = observerRegistry.add(observer)
|
|
199
|
+
|
|
200
|
+
fun removeNavigationStateObserver(observer: TabsNavigationStateObserver): Boolean = observerRegistry.remove(observer)
|
|
201
|
+
|
|
202
|
+
// endregion
|
|
203
|
+
|
|
204
|
+
// region Host-internal API
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Queue a navigation state update. Apply via [flushPendingUpdates] or rely on the
|
|
208
|
+
* host's next render cycle to apply automatically.
|
|
209
|
+
*/
|
|
210
|
+
internal fun setPendingNavigationStateUpdate(request: TabsNavigationStateUpdateRequest?) {
|
|
211
|
+
pendingStateUpdateRequest = request
|
|
212
|
+
invalidationFlags.isSelectedTabInvalidated = request != null
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
internal fun addTabsScreenAt(
|
|
216
|
+
index: Int,
|
|
217
|
+
tabsScreen: TabsScreen,
|
|
218
|
+
) {
|
|
219
|
+
tabsModel.add(index, TabsScreenFragment(tabsScreen))
|
|
220
|
+
invalidationFlags.invalidateAll()
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
internal fun removeTabsScreenAt(index: Int): TabsScreen? =
|
|
224
|
+
tabsModel.removeAt(index).tabsScreen.also {
|
|
225
|
+
invalidationFlags.invalidateAll()
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
internal fun removeTabsScreen(tabsScreen: TabsScreen): Boolean =
|
|
229
|
+
tabsModel.removeIf { it.tabsScreen === tabsScreen }.also { isRemoved ->
|
|
230
|
+
if (isRemoved) invalidationFlags.invalidateAll()
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
internal fun removeAllTabsScreens() {
|
|
234
|
+
tabsModel.clear()
|
|
235
|
+
invalidationFlags.invalidateAll()
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
internal fun setupFragmentManager() {
|
|
239
|
+
fragmentManager =
|
|
240
|
+
checkNotNull(FragmentManagerHelper.findFragmentManagerForView(this)) {
|
|
241
|
+
"[RNScreens] Nullish fragment manager - can't run container operations"
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
internal fun teardownFragmentManager() {
|
|
246
|
+
fragmentManager = null
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Idempotent teardown. Releases observer references and clears any pending operation.
|
|
251
|
+
* Called by the host on view lifecycle end. Note: named `tearDown` (not `invalidate`) to avoid
|
|
252
|
+
* shadowing [android.view.View.invalidate].
|
|
253
|
+
*/
|
|
254
|
+
internal fun tearDown() {
|
|
255
|
+
observerRegistry.clear()
|
|
256
|
+
setPendingNavigationStateUpdate(null)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// endregion
|
|
260
|
+
|
|
261
|
+
// region View lifecycle / insets / appearance
|
|
262
|
+
|
|
155
263
|
override fun onAttachedToWindow() {
|
|
156
264
|
RNSLog.d(TAG, "TabsContainer [$id] attached to window")
|
|
157
265
|
|
|
158
266
|
super.onAttachedToWindow()
|
|
159
267
|
setupFragmentManager()
|
|
160
|
-
|
|
268
|
+
flushPendingUpdates()
|
|
161
269
|
|
|
162
270
|
colorSchemeCoordinator.setup(this) { uiNightMode ->
|
|
163
271
|
applyDayNightUiMode(uiNightMode)
|
|
@@ -196,40 +304,94 @@ internal class TabsContainer(
|
|
|
196
304
|
return insets
|
|
197
305
|
}
|
|
198
306
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
307
|
+
override fun onLayoutChange(
|
|
308
|
+
view: View?,
|
|
309
|
+
left: Int,
|
|
310
|
+
top: Int,
|
|
311
|
+
right: Int,
|
|
312
|
+
bottom: Int,
|
|
313
|
+
oldLeft: Int,
|
|
314
|
+
oldTop: Int,
|
|
315
|
+
oldRight: Int,
|
|
316
|
+
oldBottom: Int,
|
|
317
|
+
) {
|
|
318
|
+
require(view is BottomNavigationView) {
|
|
319
|
+
"[RNScreens] TabsContainer's onLayoutChange expects BottomNavigationView, received $view instead"
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
val oldHeight = oldBottom - oldTop
|
|
323
|
+
val newHeight = bottom - top
|
|
324
|
+
|
|
325
|
+
if (newHeight != oldHeight) {
|
|
326
|
+
updateInterfaceInsets(newHeight)
|
|
327
|
+
}
|
|
202
328
|
}
|
|
203
329
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
invalidationFlags.invalidateAll()
|
|
330
|
+
override fun setOnInterfaceInsetsChangeListener(listener: SafeAreaView) {
|
|
331
|
+
if (interfaceInsetsChangeListener == null) {
|
|
332
|
+
bottomNavigationView.addOnLayoutChangeListener(this)
|
|
333
|
+
}
|
|
334
|
+
interfaceInsetsChangeListener = listener
|
|
210
335
|
}
|
|
211
336
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
337
|
+
override fun removeOnInterfaceInsetsChangeListener(listener: SafeAreaView) {
|
|
338
|
+
if (interfaceInsetsChangeListener == listener) {
|
|
339
|
+
interfaceInsetsChangeListener = null
|
|
340
|
+
bottomNavigationView.removeOnLayoutChangeListener(this)
|
|
215
341
|
}
|
|
342
|
+
}
|
|
216
343
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
344
|
+
override fun getInterfaceInsets(): EdgeInsets = EdgeInsets(0.0f, 0.0f, 0.0f, bottomNavigationView.height.toFloat())
|
|
345
|
+
|
|
346
|
+
override fun getResolvedUiNightMode() = colorSchemeCoordinator.getResolvedUiNightMode()
|
|
347
|
+
|
|
348
|
+
override fun addColorSchemeListener(listener: ColorSchemeListener) = colorSchemeCoordinator.addColorSchemeListener(listener)
|
|
349
|
+
|
|
350
|
+
override fun removeColorSchemeListener(listener: ColorSchemeListener) = colorSchemeCoordinator.removeColorSchemeListener(listener)
|
|
351
|
+
|
|
352
|
+
// endregion
|
|
353
|
+
|
|
354
|
+
// region TabsScreenDelegate impl
|
|
355
|
+
|
|
356
|
+
override fun onAppearanceChanged(tabsScreen: TabsScreen) {
|
|
357
|
+
if (selectedTab.tabsScreen === tabsScreen) {
|
|
358
|
+
invalidationFlags.isNavigationMenuAppearanceInvalidated = true
|
|
359
|
+
post {
|
|
360
|
+
this.flushPendingUpdates()
|
|
361
|
+
}
|
|
220
362
|
}
|
|
363
|
+
}
|
|
221
364
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
365
|
+
override fun onMenuItemAttributesChange(tabsScreen: TabsScreen) {
|
|
366
|
+
getMenuItemForTabsScreen(tabsScreen)?.let { menuItem ->
|
|
367
|
+
val appearance = selectedTab.tabsScreen.appearance
|
|
368
|
+
appearanceCoordinator.updateMenuItemAppearance(
|
|
369
|
+
themedContext,
|
|
370
|
+
menuItem,
|
|
371
|
+
tabsScreen,
|
|
372
|
+
appearance,
|
|
373
|
+
)
|
|
374
|
+
a11yCoordinator.setA11yPropertiesToTabItem(menuItem, tabsScreen)
|
|
375
|
+
}
|
|
225
376
|
}
|
|
226
377
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
378
|
+
override fun getFragmentForTabsScreen(tabsScreen: TabsScreen): TabsScreenFragment? =
|
|
379
|
+
tabsModel.find {
|
|
380
|
+
it.tabsScreen ===
|
|
381
|
+
tabsScreen
|
|
230
382
|
}
|
|
383
|
+
|
|
384
|
+
override fun onFragmentConfigurationChange(
|
|
385
|
+
tabsScreen: TabsScreen,
|
|
386
|
+
config: Configuration,
|
|
387
|
+
) {
|
|
388
|
+
this.onConfigurationChanged(config)
|
|
231
389
|
}
|
|
232
390
|
|
|
391
|
+
// endregion
|
|
392
|
+
|
|
393
|
+
// region Private helpers
|
|
394
|
+
|
|
233
395
|
private fun performContainerUpdate() {
|
|
234
396
|
if (invalidationFlags.isNavigationMenuStructureInvalidated) {
|
|
235
397
|
invalidationFlags.isNavigationMenuStructureInvalidated = false
|
|
@@ -249,27 +411,25 @@ internal class TabsContainer(
|
|
|
249
411
|
}
|
|
250
412
|
|
|
251
413
|
private fun performOperation() {
|
|
252
|
-
if (
|
|
414
|
+
if (pendingStateUpdateRequest == null) {
|
|
253
415
|
RNSLog.w(TAG, "TabsContainer::performOperation called w/o pending operation; skipping update")
|
|
254
416
|
return
|
|
255
417
|
}
|
|
256
418
|
|
|
257
|
-
|
|
258
|
-
check(pendingOperation is TabSelectOp)
|
|
259
|
-
val tabSelectOp = pendingOperation as TabSelectOp
|
|
419
|
+
val stateUpdateRequest = requirePendingStateUpdateRequest()
|
|
260
420
|
|
|
261
421
|
val nextSelectedMenuItemId =
|
|
262
|
-
checkNotNull(getMenuItemIdForFragment(requireFragmentForScreenKey(
|
|
263
|
-
"[RNScreens] Failed to find Menu Item for screenKey: ${
|
|
422
|
+
checkNotNull(getMenuItemIdForFragment(requireFragmentForScreenKey(stateUpdateRequest.selectedScreenKey))) {
|
|
423
|
+
"[RNScreens] Failed to find Menu Item for screenKey: ${stateUpdateRequest.selectedScreenKey}"
|
|
264
424
|
}
|
|
265
425
|
|
|
266
|
-
if (
|
|
267
|
-
|
|
426
|
+
if (rejectStaleNavigationStateUpdates && isNavigationStateStale(stateUpdateRequest)) {
|
|
427
|
+
observerRegistry.emitOnNavigationStateUpdateRejected(
|
|
268
428
|
navState,
|
|
269
|
-
|
|
270
|
-
|
|
429
|
+
stateUpdateRequest,
|
|
430
|
+
TabsNavigationStateRejectionReason.STALE,
|
|
271
431
|
)
|
|
272
|
-
|
|
432
|
+
pendingStateUpdateRequest = null
|
|
273
433
|
return
|
|
274
434
|
}
|
|
275
435
|
|
|
@@ -279,14 +439,14 @@ internal class TabsContainer(
|
|
|
279
439
|
bottomNavigationView.selectedItemId = nextSelectedMenuItemId
|
|
280
440
|
isInExternalOperationContext = false
|
|
281
441
|
} else {
|
|
282
|
-
|
|
442
|
+
observerRegistry.emitOnNavigationStateUpdateRejected(
|
|
283
443
|
navState,
|
|
284
|
-
|
|
285
|
-
|
|
444
|
+
stateUpdateRequest,
|
|
445
|
+
TabsNavigationStateRejectionReason.REPEATED,
|
|
286
446
|
)
|
|
287
447
|
}
|
|
288
448
|
|
|
289
|
-
|
|
449
|
+
pendingStateUpdateRequest = null
|
|
290
450
|
}
|
|
291
451
|
|
|
292
452
|
private fun updateNavigationMenuStructure() {
|
|
@@ -316,8 +476,8 @@ internal class TabsContainer(
|
|
|
316
476
|
|
|
317
477
|
private fun updateSelectedFragment(nextSelectedFragment: TabsScreenFragment): Boolean {
|
|
318
478
|
if (navState.isEmpty()) {
|
|
319
|
-
check(isInExternalOperationContext &&
|
|
320
|
-
navState =
|
|
479
|
+
check(isInExternalOperationContext && pendingStateUpdateRequest != null)
|
|
480
|
+
navState = TabsNavigationState(nextSelectedFragment.requireScreenKey, 0)
|
|
321
481
|
requireFragmentManager
|
|
322
482
|
.createTransactionWithReordering()
|
|
323
483
|
.add(contentView.id, nextSelectedFragment)
|
|
@@ -328,7 +488,7 @@ internal class TabsContainer(
|
|
|
328
488
|
val currentSelectedFragment = selectedTab
|
|
329
489
|
|
|
330
490
|
if (nextSelectedFragment === currentSelectedFragment) {
|
|
331
|
-
progressNavigationState(navState.
|
|
491
|
+
progressNavigationState(navState.selectedScreenKey)
|
|
332
492
|
return true
|
|
333
493
|
}
|
|
334
494
|
|
|
@@ -343,8 +503,8 @@ internal class TabsContainer(
|
|
|
343
503
|
return true
|
|
344
504
|
}
|
|
345
505
|
|
|
346
|
-
private fun progressNavigationState(
|
|
347
|
-
navState =
|
|
506
|
+
private fun progressNavigationState(selectedScreenKey: String) {
|
|
507
|
+
navState = TabsNavigationState(selectedScreenKey, navState.provenance + 1)
|
|
348
508
|
if (!isInExternalOperationContext) {
|
|
349
509
|
lastUINavState = navState
|
|
350
510
|
}
|
|
@@ -363,7 +523,7 @@ internal class TabsContainer(
|
|
|
363
523
|
|
|
364
524
|
// If this is user action we test whether it should be prevented before we progress the state.
|
|
365
525
|
if (!isRepeated && !isInExternalOperationContext && nextSelectedFragment.isPreventNativeSelectionEnabled) {
|
|
366
|
-
|
|
526
|
+
observerRegistry.emitOnNavigationStateUpdatePrevented(navState, nextSelectedFragment.requireScreenKey)
|
|
367
527
|
return false
|
|
368
528
|
}
|
|
369
529
|
|
|
@@ -373,11 +533,16 @@ internal class TabsContainer(
|
|
|
373
533
|
if (isRepeated) specialEffectsHandler.handleRepeatedTabSelection() else false
|
|
374
534
|
|
|
375
535
|
if (stateChanged) {
|
|
376
|
-
|
|
536
|
+
observerRegistry.emitOnNavigationStateUpdate(
|
|
377
537
|
navState,
|
|
378
538
|
isRepeated = isRepeated,
|
|
379
539
|
hasTriggeredSpecialEffect = hasTriggeredSpecialEffect,
|
|
380
|
-
|
|
540
|
+
actionOrigin =
|
|
541
|
+
if (isInExternalOperationContext) {
|
|
542
|
+
requirePendingStateUpdateRequest().actionOrigin
|
|
543
|
+
} else {
|
|
544
|
+
TabsActionOrigin.USER
|
|
545
|
+
},
|
|
381
546
|
)
|
|
382
547
|
}
|
|
383
548
|
|
|
@@ -413,7 +578,7 @@ internal class TabsContainer(
|
|
|
413
578
|
|
|
414
579
|
private fun getSelectedTabsScreenFragmentId(): Int? =
|
|
415
580
|
tabsModel
|
|
416
|
-
.indexOfFirst { it.requireScreenKey == navState.
|
|
581
|
+
.indexOfFirst { it.requireScreenKey == navState.selectedScreenKey }
|
|
417
582
|
.takeIf { it != -1 }
|
|
418
583
|
|
|
419
584
|
private fun getMenuItemForTabsScreen(tabsScreen: TabsScreen): MenuItem? =
|
|
@@ -424,40 +589,6 @@ internal class TabsContainer(
|
|
|
424
589
|
bottomNavigationView.menu.findItem(menuItemIdForFragmentAtIndex(index))
|
|
425
590
|
}
|
|
426
591
|
|
|
427
|
-
override fun getResolvedUiNightMode() = colorSchemeCoordinator.getResolvedUiNightMode()
|
|
428
|
-
|
|
429
|
-
override fun addColorSchemeListener(listener: ColorSchemeListener) = colorSchemeCoordinator.addColorSchemeListener(listener)
|
|
430
|
-
|
|
431
|
-
override fun removeColorSchemeListener(listener: ColorSchemeListener) = colorSchemeCoordinator.removeColorSchemeListener(listener)
|
|
432
|
-
|
|
433
|
-
override fun onAppearanceChanged(tabsScreen: TabsScreen) {
|
|
434
|
-
if (selectedTab.tabsScreen === tabsScreen) {
|
|
435
|
-
invalidationFlags.isNavigationMenuAppearanceInvalidated = true
|
|
436
|
-
post {
|
|
437
|
-
this.performContainerUpdateIfNeeded()
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
override fun onMenuItemAttributesChange(tabsScreen: TabsScreen) {
|
|
443
|
-
getMenuItemForTabsScreen(tabsScreen)?.let { menuItem ->
|
|
444
|
-
val appearance = selectedTab.tabsScreen.appearance
|
|
445
|
-
appearanceCoordinator.updateMenuItemAppearance(
|
|
446
|
-
themedContext,
|
|
447
|
-
menuItem,
|
|
448
|
-
tabsScreen,
|
|
449
|
-
appearance,
|
|
450
|
-
)
|
|
451
|
-
a11yCoordinator.setA11yPropertiesToTabItem(menuItem, tabsScreen)
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
override fun getFragmentForTabsScreen(tabsScreen: TabsScreen): TabsScreenFragment? =
|
|
456
|
-
tabsModel.find {
|
|
457
|
-
it.tabsScreen ===
|
|
458
|
-
tabsScreen
|
|
459
|
-
}
|
|
460
|
-
|
|
461
592
|
private fun getFragmentForScreenKey(screenKey: String): TabsScreenFragment? = tabsModel.find { it.requireScreenKey == screenKey }
|
|
462
593
|
|
|
463
594
|
private fun requireFragmentForScreenKey(screenKey: String): TabsScreenFragment =
|
|
@@ -465,50 +596,6 @@ internal class TabsContainer(
|
|
|
465
596
|
"[RNScreens] Requested fragment for key: $screenKey does not exist"
|
|
466
597
|
}
|
|
467
598
|
|
|
468
|
-
override fun onFragmentConfigurationChange(
|
|
469
|
-
tabsScreen: TabsScreen,
|
|
470
|
-
config: Configuration,
|
|
471
|
-
) {
|
|
472
|
-
this.onConfigurationChanged(config)
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
override fun onLayoutChange(
|
|
476
|
-
view: View?,
|
|
477
|
-
left: Int,
|
|
478
|
-
top: Int,
|
|
479
|
-
right: Int,
|
|
480
|
-
bottom: Int,
|
|
481
|
-
oldLeft: Int,
|
|
482
|
-
oldTop: Int,
|
|
483
|
-
oldRight: Int,
|
|
484
|
-
oldBottom: Int,
|
|
485
|
-
) {
|
|
486
|
-
require(view is BottomNavigationView) {
|
|
487
|
-
"[RNScreens] TabsContainer's onLayoutChange expects BottomNavigationView, received $view instead"
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
val oldHeight = oldBottom - oldTop
|
|
491
|
-
val newHeight = bottom - top
|
|
492
|
-
|
|
493
|
-
if (newHeight != oldHeight) {
|
|
494
|
-
updateInterfaceInsets(newHeight)
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
override fun setOnInterfaceInsetsChangeListener(listener: SafeAreaView) {
|
|
499
|
-
if (interfaceInsetsChangeListener == null) {
|
|
500
|
-
bottomNavigationView.addOnLayoutChangeListener(this)
|
|
501
|
-
}
|
|
502
|
-
interfaceInsetsChangeListener = listener
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
override fun removeOnInterfaceInsetsChangeListener(listener: SafeAreaView) {
|
|
506
|
-
if (interfaceInsetsChangeListener == listener) {
|
|
507
|
-
interfaceInsetsChangeListener = null
|
|
508
|
-
bottomNavigationView.removeOnLayoutChangeListener(this)
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
|
|
512
599
|
private fun updateInterfaceInsets(newHeight: Int? = null) {
|
|
513
600
|
val height = if (tabBarHidden) 0 else (newHeight ?: bottomNavigationView.height)
|
|
514
601
|
|
|
@@ -517,8 +604,6 @@ internal class TabsContainer(
|
|
|
517
604
|
}
|
|
518
605
|
}
|
|
519
606
|
|
|
520
|
-
override fun getInterfaceInsets(): EdgeInsets = EdgeInsets(0.0f, 0.0f, 0.0f, bottomNavigationView.height.toFloat())
|
|
521
|
-
|
|
522
607
|
private fun getInsetsForBottomNavigationView(insets: WindowInsets): WindowInsets? {
|
|
523
608
|
if (tabBarRespectsIMEInsets) {
|
|
524
609
|
return insets
|
|
@@ -533,22 +618,13 @@ internal class TabsContainer(
|
|
|
533
618
|
.toWindowInsets()
|
|
534
619
|
}
|
|
535
620
|
|
|
536
|
-
|
|
537
|
-
fragmentManager =
|
|
538
|
-
checkNotNull(FragmentManagerHelper.findFragmentManagerForView(this)) {
|
|
539
|
-
"[RNScreens] Nullish fragment manager - can't run container operations"
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
internal fun teardownFragmentManager() {
|
|
544
|
-
fragmentManager = null
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
private fun isNavStateStale(state: TabsNavState): Boolean {
|
|
621
|
+
private fun isNavigationStateStale(request: TabsNavigationStateUpdateRequest): Boolean {
|
|
548
622
|
if (navState.isEmpty() || lastUINavState.isEmpty()) return false
|
|
549
|
-
return
|
|
623
|
+
return request.baseProvenance < lastUINavState.provenance
|
|
550
624
|
}
|
|
551
625
|
|
|
626
|
+
// endregion
|
|
627
|
+
|
|
552
628
|
companion object {
|
|
553
629
|
const val TAG = "TabsContainer"
|
|
554
630
|
}
|
package/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/TabsNavigationState.kt
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
package com.swmansion.rnscreens.gamma.tabs.container
|
|
2
|
+
|
|
3
|
+
import com.swmansion.rnscreens.gamma.tabs.container.TabsNavigationStateRejectionReason.REPEATED
|
|
4
|
+
import com.swmansion.rnscreens.gamma.tabs.container.TabsNavigationStateRejectionReason.STALE
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Describes navigation state of a tabs container.
|
|
8
|
+
*
|
|
9
|
+
* @property selectedScreenKey Screen key of the currently selected tab.
|
|
10
|
+
* @property provenance Monotonically increasing number describing the history (generation) of the state.
|
|
11
|
+
* State with provenance `N + 1` is derived from state with provenance `N`.
|
|
12
|
+
* This allows detecting stale updates.
|
|
13
|
+
*/
|
|
14
|
+
data class TabsNavigationState(
|
|
15
|
+
val selectedScreenKey: String,
|
|
16
|
+
val provenance: Int,
|
|
17
|
+
) {
|
|
18
|
+
internal fun isEmpty(): Boolean = this === EMPTY
|
|
19
|
+
|
|
20
|
+
internal fun isNotEmpty(): Boolean = !this.isEmpty()
|
|
21
|
+
|
|
22
|
+
companion object {
|
|
23
|
+
val EMPTY = TabsNavigationState("", Int.MIN_VALUE)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A request to change navigation state.
|
|
29
|
+
*
|
|
30
|
+
* Carries the target [selectedScreenKey], the [baseProvenance] of the state the request was derived from,
|
|
31
|
+
* and the [actionOrigin] (actor) that initiated it. Mirrors the public `TabsHostNavStateRequest` TS type
|
|
32
|
+
* plus an [actionOrigin] carried internally.
|
|
33
|
+
*
|
|
34
|
+
* @property selectedScreenKey Screen key of the requested tab.
|
|
35
|
+
* @property baseProvenance Provenance of the state this request was derived from. Used for staleness detection.
|
|
36
|
+
* @property actionOrigin Origin (actor) that initiated this request.
|
|
37
|
+
*/
|
|
38
|
+
data class TabsNavigationStateUpdateRequest(
|
|
39
|
+
val selectedScreenKey: String,
|
|
40
|
+
val baseProvenance: Int,
|
|
41
|
+
val actionOrigin: TabsActionOrigin,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Reason why a navigation state update was rejected by the container.
|
|
46
|
+
*
|
|
47
|
+
* - [STALE] — the update's provenance indicates that it is based on a stale state.
|
|
48
|
+
* - [REPEATED] — the requested tab is already selected.
|
|
49
|
+
*/
|
|
50
|
+
enum class TabsNavigationStateRejectionReason {
|
|
51
|
+
STALE,
|
|
52
|
+
REPEATED,
|
|
53
|
+
;
|
|
54
|
+
|
|
55
|
+
override fun toString(): String =
|
|
56
|
+
when (this) {
|
|
57
|
+
STALE -> "stale"
|
|
58
|
+
REPEATED -> "repeated"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -1,36 +1,41 @@
|
|
|
1
1
|
package com.swmansion.rnscreens.gamma.tabs.container
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Observer of navigation state changes on a [TabsContainer].
|
|
5
|
+
*
|
|
6
|
+
* Multiple observers may register against a single container via
|
|
7
|
+
* [TabsContainer.addNavigationStateObserver] / [TabsContainer.removeNavigationStateObserver].
|
|
8
|
+
* The host (`TabsHost` on Android) registers itself as an observer to relay events to JS;
|
|
9
|
+
* downstream native libraries integrating directly against [TabsContainer] may register
|
|
10
|
+
* additional observers.
|
|
6
11
|
*/
|
|
7
|
-
|
|
12
|
+
interface TabsNavigationStateObserver {
|
|
8
13
|
/**
|
|
9
14
|
* Called when the container accepts a navigation state change.
|
|
10
15
|
*
|
|
11
16
|
* @param navState The new navigation state after the change.
|
|
12
17
|
* @param isRepeated Whether the same tab that was already selected has been selected again.
|
|
13
18
|
* @param hasTriggeredSpecialEffect Whether a special effect (e.g. scroll-to-top) was triggered.
|
|
14
|
-
* @param
|
|
19
|
+
* @param actionOrigin Origin (actor) that requested this transition.
|
|
15
20
|
*/
|
|
16
|
-
fun
|
|
17
|
-
navState:
|
|
21
|
+
fun onNavigationStateUpdate(
|
|
22
|
+
navState: TabsNavigationState,
|
|
18
23
|
isRepeated: Boolean,
|
|
19
24
|
hasTriggeredSpecialEffect: Boolean,
|
|
20
|
-
|
|
25
|
+
actionOrigin: TabsActionOrigin,
|
|
21
26
|
)
|
|
22
27
|
|
|
23
28
|
/**
|
|
24
29
|
* Called when the container rejects a navigation state update.
|
|
25
30
|
*
|
|
26
31
|
* @param currentNavState The currently active navigation state that was kept.
|
|
27
|
-
* @param
|
|
32
|
+
* @param rejectedRequest The navigation state update request that was rejected.
|
|
28
33
|
* @param reason Why the update was rejected.
|
|
29
34
|
*/
|
|
30
|
-
fun
|
|
31
|
-
currentNavState:
|
|
32
|
-
|
|
33
|
-
reason:
|
|
35
|
+
fun onNavigationStateUpdateRejected(
|
|
36
|
+
currentNavState: TabsNavigationState,
|
|
37
|
+
rejectedRequest: TabsNavigationStateUpdateRequest,
|
|
38
|
+
reason: TabsNavigationStateRejectionReason,
|
|
34
39
|
)
|
|
35
40
|
|
|
36
41
|
/**
|
|
@@ -41,8 +46,8 @@ internal interface TabsContainerDelegate {
|
|
|
41
46
|
* @param currentNavState The currently active navigation state that was kept.
|
|
42
47
|
* @param preventedScreenKey The screen key of the tab whose selection was prevented.
|
|
43
48
|
*/
|
|
44
|
-
fun
|
|
45
|
-
currentNavState:
|
|
49
|
+
fun onNavigationStateUpdatePrevented(
|
|
50
|
+
currentNavState: TabsNavigationState,
|
|
46
51
|
preventedScreenKey: String,
|
|
47
52
|
)
|
|
48
53
|
}
|