@onekeyfe/react-native-tab-view 1.1.31

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 (94) hide show
  1. package/android/build.gradle +119 -0
  2. package/android/gradle.properties +4 -0
  3. package/android/src/main/AndroidManifest.xml +3 -0
  4. package/android/src/main/AndroidManifestNew.xml +2 -0
  5. package/android/src/main/java/com/rcttabview/ImageSource.kt +86 -0
  6. package/android/src/main/java/com/rcttabview/RCTTabView.kt +529 -0
  7. package/android/src/main/java/com/rcttabview/RCTTabViewManager.kt +204 -0
  8. package/android/src/main/java/com/rcttabview/RCTTabViewPackage.kt +16 -0
  9. package/android/src/main/java/com/rcttabview/TabInfo.kt +12 -0
  10. package/android/src/main/java/com/rcttabview/Utils.kt +31 -0
  11. package/android/src/main/java/com/rcttabview/events/OnNativeLayoutEvent.kt +20 -0
  12. package/android/src/main/java/com/rcttabview/events/OnTabBarMeasuredEvent.kt +19 -0
  13. package/android/src/main/java/com/rcttabview/events/PageSelectedEvent.kt +21 -0
  14. package/android/src/main/java/com/rcttabview/events/TabLongPressedEvent.kt +19 -0
  15. package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewComponentDescriptor.h +32 -0
  16. package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.cpp +18 -0
  17. package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.h +35 -0
  18. package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewState.cpp +15 -0
  19. package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewState.h +25 -0
  20. package/ios/Extensions.swift +46 -0
  21. package/ios/RCTBottomAccessoryComponentView.h +12 -0
  22. package/ios/RCTBottomAccessoryComponentView.mm +67 -0
  23. package/ios/RCTBottomAccessoryContainerView.swift +51 -0
  24. package/ios/RCTTabViewComponentView.h +12 -0
  25. package/ios/RCTTabViewComponentView.mm +325 -0
  26. package/ios/RCTTabViewContainerView.swift +768 -0
  27. package/ios/RCTTabViewLog.h +7 -0
  28. package/ios/RCTTabViewLog.m +32 -0
  29. package/ios/SVG/CoreSVG.h +13 -0
  30. package/ios/SVG/CoreSVG.mm +177 -0
  31. package/ios/SVG/SvgDecoder.h +10 -0
  32. package/ios/SVG/SvgDecoder.mm +32 -0
  33. package/ios/TabBarFontSize.swift +55 -0
  34. package/lib/module/BottomAccessoryView.js +45 -0
  35. package/lib/module/BottomAccessoryView.js.map +1 -0
  36. package/lib/module/BottomAccessoryViewNativeComponent.ts +27 -0
  37. package/lib/module/DelayedFreeze.js +26 -0
  38. package/lib/module/DelayedFreeze.js.map +1 -0
  39. package/lib/module/NativeSVGDecoder.js +5 -0
  40. package/lib/module/NativeSVGDecoder.js.map +1 -0
  41. package/lib/module/SceneMap.js +28 -0
  42. package/lib/module/SceneMap.js.map +1 -0
  43. package/lib/module/TabView.js +263 -0
  44. package/lib/module/TabView.js.map +1 -0
  45. package/lib/module/TabViewNativeComponent.ts +68 -0
  46. package/lib/module/codegen-types.d.js +2 -0
  47. package/lib/module/codegen-types.d.js.map +1 -0
  48. package/lib/module/index.js +20 -0
  49. package/lib/module/index.js.map +1 -0
  50. package/lib/module/package.json +1 -0
  51. package/lib/module/types.js +4 -0
  52. package/lib/module/types.js.map +1 -0
  53. package/lib/module/utils/BottomTabBarHeightContext.js +5 -0
  54. package/lib/module/utils/BottomTabBarHeightContext.js.map +1 -0
  55. package/lib/module/utils/useBottomTabBarHeight.js +12 -0
  56. package/lib/module/utils/useBottomTabBarHeight.js.map +1 -0
  57. package/lib/typescript/package.json +1 -0
  58. package/lib/typescript/src/BottomAccessoryView.d.ts +8 -0
  59. package/lib/typescript/src/BottomAccessoryView.d.ts.map +1 -0
  60. package/lib/typescript/src/BottomAccessoryViewNativeComponent.d.ts +16 -0
  61. package/lib/typescript/src/BottomAccessoryViewNativeComponent.d.ts.map +1 -0
  62. package/lib/typescript/src/DelayedFreeze.d.ts +8 -0
  63. package/lib/typescript/src/DelayedFreeze.d.ts.map +1 -0
  64. package/lib/typescript/src/NativeSVGDecoder.d.ts +6 -0
  65. package/lib/typescript/src/NativeSVGDecoder.d.ts.map +1 -0
  66. package/lib/typescript/src/SceneMap.d.ts +10 -0
  67. package/lib/typescript/src/SceneMap.d.ts.map +1 -0
  68. package/lib/typescript/src/TabView.d.ts +178 -0
  69. package/lib/typescript/src/TabView.d.ts.map +1 -0
  70. package/lib/typescript/src/TabViewNativeComponent.d.ts +55 -0
  71. package/lib/typescript/src/TabViewNativeComponent.d.ts.map +1 -0
  72. package/lib/typescript/src/index.d.ts +16 -0
  73. package/lib/typescript/src/index.d.ts.map +1 -0
  74. package/lib/typescript/src/types.d.ts +29 -0
  75. package/lib/typescript/src/types.d.ts.map +1 -0
  76. package/lib/typescript/src/utils/BottomTabBarHeightContext.d.ts +3 -0
  77. package/lib/typescript/src/utils/BottomTabBarHeightContext.d.ts.map +1 -0
  78. package/lib/typescript/src/utils/useBottomTabBarHeight.d.ts +2 -0
  79. package/lib/typescript/src/utils/useBottomTabBarHeight.d.ts.map +1 -0
  80. package/package.json +114 -0
  81. package/react-native-tab-view.podspec +36 -0
  82. package/react-native.config.js +13 -0
  83. package/src/BottomAccessoryView.tsx +58 -0
  84. package/src/BottomAccessoryViewNativeComponent.ts +27 -0
  85. package/src/DelayedFreeze.tsx +27 -0
  86. package/src/NativeSVGDecoder.ts +5 -0
  87. package/src/SceneMap.tsx +34 -0
  88. package/src/TabView.tsx +466 -0
  89. package/src/TabViewNativeComponent.ts +68 -0
  90. package/src/codegen-types.d.ts +28 -0
  91. package/src/index.tsx +18 -0
  92. package/src/types.ts +31 -0
  93. package/src/utils/BottomTabBarHeightContext.ts +5 -0
  94. package/src/utils/useBottomTabBarHeight.ts +15 -0
@@ -0,0 +1,529 @@
1
+ package com.rcttabview
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.content.Context
5
+ import android.content.res.ColorStateList
6
+ import android.content.res.Configuration
7
+ import androidx.appcompat.view.ContextThemeWrapper
8
+ import android.graphics.drawable.ColorDrawable
9
+ import android.graphics.drawable.Drawable
10
+ import android.os.Build
11
+ import android.transition.TransitionManager
12
+ import android.util.Log
13
+ import android.util.Size
14
+ import android.util.TypedValue
15
+ import android.view.Choreographer
16
+ import android.view.HapticFeedbackConstants
17
+ import android.view.MenuItem
18
+ import android.view.View
19
+ import android.view.ViewGroup
20
+ import android.widget.FrameLayout
21
+ import android.widget.LinearLayout
22
+ import android.widget.TextView
23
+ import androidx.core.view.forEachIndexed
24
+ import coil3.ImageLoader
25
+ import coil3.asDrawable
26
+ import coil3.request.ImageRequest
27
+ import coil3.svg.SvgDecoder
28
+ import coil3.size.Precision
29
+ import coil3.size.Size as CoilSize
30
+ import coil3.size.Scale
31
+ import com.facebook.react.bridge.ReadableArray
32
+ import com.facebook.react.common.assets.ReactFontManager
33
+ import com.facebook.react.modules.core.ReactChoreographer
34
+ import com.facebook.react.views.text.ReactTypefaceUtils
35
+ import com.google.android.material.bottomnavigation.BottomNavigationView
36
+ import com.google.android.material.navigation.NavigationBarView.LABEL_VISIBILITY_AUTO
37
+ import com.google.android.material.navigation.NavigationBarView.LABEL_VISIBILITY_LABELED
38
+ import com.google.android.material.navigation.NavigationBarView.LABEL_VISIBILITY_UNLABELED
39
+ import com.google.android.material.transition.platform.MaterialFadeThrough
40
+
41
+ private fun getMaterialContext(context: Context): Context {
42
+ return ContextThemeWrapper(context, com.google.android.material.R.style.Theme_MaterialComponents_DayNight)
43
+ }
44
+
45
+ class ExtendedBottomNavigationView(context: Context) : BottomNavigationView(getMaterialContext(context)) {
46
+
47
+ fun setIgnoreBottomInsets(ignore: Boolean) {
48
+ if (ignore) {
49
+ setOnApplyWindowInsetsListener { v, insets -> insets }
50
+ setPadding(paddingLeft, paddingTop, paddingRight, 0)
51
+ } else {
52
+ setOnApplyWindowInsetsListener(null)
53
+ requestApplyInsets()
54
+ }
55
+ }
56
+
57
+ override fun getMaxItemCount(): Int {
58
+ return 100
59
+ }
60
+ }
61
+
62
+ class ReactBottomNavigationView(context: Context) : LinearLayout(context) {
63
+ private var bottomNavigation = ExtendedBottomNavigationView(context)
64
+ val layoutHolder = FrameLayout(context)
65
+
66
+ var onTabSelectedListener: ((key: String) -> Unit)? = null
67
+ var onTabLongPressedListener: ((key: String) -> Unit)? = null
68
+ var onNativeLayoutListener: ((width: Double, height: Double) -> Unit)? = null
69
+ var onTabBarMeasuredListener: ((height: Int) -> Unit)? = null
70
+ var disablePageAnimations = false
71
+ var items: MutableList<TabInfo> = mutableListOf()
72
+ private val iconSources: MutableMap<Int, ImageSource> = mutableMapOf()
73
+ private val drawableCache: MutableMap<ImageSource, Drawable> = mutableMapOf()
74
+
75
+ private var isLayoutEnqueued = false
76
+ private var selectedItem: String? = null
77
+ private var activeTintColor: Int? = null
78
+ private var inactiveTintColor: Int? = null
79
+ private val checkedStateSet = intArrayOf(android.R.attr.state_checked)
80
+ private val uncheckedStateSet = intArrayOf(-android.R.attr.state_checked)
81
+ private var hapticFeedbackEnabled = false
82
+ private var fontSize: Int? = null
83
+ private var fontFamily: String? = null
84
+ private var fontWeight: Int? = null
85
+ private var labeled: Boolean? = null
86
+ private var lastReportedSize: Size? = null
87
+ private var hasCustomAppearance = false
88
+ private var uiModeConfiguration: Int = Configuration.UI_MODE_NIGHT_UNDEFINED
89
+
90
+ private val imageLoader = ImageLoader.Builder(context)
91
+ .components {
92
+ add(SvgDecoder.Factory())
93
+ }
94
+ .build()
95
+
96
+ init {
97
+ orientation = VERTICAL
98
+
99
+ addView(
100
+ layoutHolder, LayoutParams(
101
+ LayoutParams.MATCH_PARENT,
102
+ 0,
103
+ ).apply { weight = 1f }
104
+ )
105
+ layoutHolder.isSaveEnabled = false
106
+
107
+ addView(bottomNavigation, LayoutParams(
108
+ LayoutParams.MATCH_PARENT,
109
+ LayoutParams.WRAP_CONTENT
110
+ ))
111
+ uiModeConfiguration = resources.configuration.uiMode
112
+
113
+ post {
114
+ addOnLayoutChangeListener { _, left, top, right, bottom,
115
+ _, _, _, _ ->
116
+ val newWidth = right - left
117
+ val newHeight = bottom - top
118
+
119
+ // Notify about tab bar height.
120
+ onTabBarMeasuredListener?.invoke(Utils.convertPixelsToDp(context, bottomNavigation.height).toInt())
121
+
122
+ if (newWidth != lastReportedSize?.width || newHeight != lastReportedSize?.height) {
123
+ val dpWidth = Utils.convertPixelsToDp(context, layoutHolder.width)
124
+ val dpHeight = Utils.convertPixelsToDp(context, layoutHolder.height)
125
+
126
+ onNativeLayoutListener?.invoke(dpWidth, dpHeight)
127
+ lastReportedSize = Size(newWidth, newHeight)
128
+ }
129
+ }
130
+ }
131
+ }
132
+
133
+ private val layoutCallback = Choreographer.FrameCallback {
134
+ isLayoutEnqueued = false
135
+ refreshLayout()
136
+ }
137
+
138
+ private fun refreshLayout() {
139
+ measure(
140
+ MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
141
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY),
142
+ )
143
+ layout(left, top, right, bottom)
144
+ }
145
+
146
+ override fun requestLayout() {
147
+ super.requestLayout()
148
+ @Suppress("SENSELESS_COMPARISON") // layoutCallback can be null here since this method can be called in init
149
+
150
+ if (!isLayoutEnqueued && layoutCallback != null) {
151
+ isLayoutEnqueued = true
152
+ // we use NATIVE_ANIMATED_MODULE choreographer queue because it allows us to catch the current
153
+ // looper loop instead of enqueueing the update in the next loop causing a one frame delay.
154
+ ReactChoreographer
155
+ .getInstance()
156
+ .postFrameCallback(
157
+ ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE,
158
+ layoutCallback,
159
+ )
160
+ }
161
+ }
162
+
163
+ fun setSelectedItem(value: String) {
164
+ selectedItem = value
165
+ setSelectedIndex(items.indexOfFirst { it.key == value })
166
+ }
167
+
168
+ override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams?) {
169
+ if (child === layoutHolder || child === bottomNavigation) {
170
+ super.addView(child, index, params)
171
+ return
172
+ }
173
+
174
+ val container = createContainer()
175
+ container.addView(child, params)
176
+ layoutHolder.addView(container, index)
177
+
178
+ val itemKey = items[index].key
179
+ if (selectedItem == itemKey) {
180
+ setSelectedIndex(index)
181
+ refreshLayout()
182
+ }
183
+ }
184
+
185
+ private fun createContainer(): FrameLayout {
186
+ val container = FrameLayout(context).apply {
187
+ layoutParams = FrameLayout.LayoutParams(
188
+ FrameLayout.LayoutParams.MATCH_PARENT,
189
+ FrameLayout.LayoutParams.MATCH_PARENT
190
+ )
191
+ isSaveEnabled = false
192
+ visibility = GONE
193
+ isEnabled = false
194
+ }
195
+ return container
196
+ }
197
+
198
+ private fun setSelectedIndex(itemId: Int) {
199
+ bottomNavigation.selectedItemId = itemId
200
+ if (!disablePageAnimations) {
201
+ val fadeThrough = MaterialFadeThrough()
202
+ TransitionManager.beginDelayedTransition(layoutHolder, fadeThrough)
203
+ }
204
+
205
+ layoutHolder.forEachIndexed { index, view ->
206
+ if (itemId == index) {
207
+ toggleViewVisibility(view, true)
208
+ } else {
209
+ toggleViewVisibility(view, false)
210
+ }
211
+ }
212
+
213
+ layoutHolder.requestLayout()
214
+ layoutHolder.invalidate()
215
+ }
216
+
217
+ private fun toggleViewVisibility(view: View, isVisible: Boolean) {
218
+ check(view is ViewGroup) { "Native component tree is corrupted." }
219
+
220
+ view.visibility = if (isVisible) VISIBLE else GONE
221
+ view.isEnabled = isVisible
222
+ }
223
+
224
+ private fun onTabSelected(item: MenuItem) {
225
+ val selectedItem = items[item.itemId]
226
+ selectedItem.let {
227
+ onTabSelectedListener?.invoke(selectedItem.key)
228
+ emitHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
229
+ }
230
+ }
231
+
232
+ private fun onTabLongPressed(item: MenuItem) {
233
+ val longPressedItem = items[item.itemId]
234
+ longPressedItem.let {
235
+ onTabLongPressedListener?.invoke(longPressedItem.key)
236
+ emitHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
237
+ }
238
+ }
239
+
240
+ fun setIgnoreBottomInsets(ignore: Boolean) {
241
+ bottomNavigation.setIgnoreBottomInsets(ignore)
242
+ }
243
+
244
+ fun setTabBarHidden(isHidden: Boolean) {
245
+ if (isHidden) {
246
+ bottomNavigation.visibility = GONE
247
+ } else {
248
+ bottomNavigation.visibility = VISIBLE
249
+ }
250
+
251
+ // Force re-measure and notify JS about the new layout dimensions
252
+ post {
253
+ val dpWidth = Utils.convertPixelsToDp(context, layoutHolder.width)
254
+ val dpHeight = Utils.convertPixelsToDp(context, layoutHolder.height)
255
+ onNativeLayoutListener?.invoke(dpWidth, dpHeight)
256
+
257
+ // Update tab bar height measurement (0 when hidden)
258
+ val tabBarHeight = if (isHidden) 0 else Utils.convertPixelsToDp(context, bottomNavigation.height).toInt()
259
+ onTabBarMeasuredListener?.invoke(tabBarHeight)
260
+ }
261
+ }
262
+
263
+ fun updateItems(items: MutableList<TabInfo>) {
264
+ // If an item got removed, let's re-add all items
265
+ if (items.size < this.items.size) {
266
+ bottomNavigation.menu.clear()
267
+ }
268
+ this.items = items
269
+ items.forEachIndexed { index, item ->
270
+ val menuItem = getOrCreateItem(index, item.title)
271
+ if (item.title !== menuItem.title) {
272
+ menuItem.title = item.title
273
+ }
274
+
275
+ menuItem.isVisible = !item.hidden
276
+ if (iconSources.containsKey(index)) {
277
+ getDrawable(iconSources[index]!!) {
278
+ menuItem.icon = it
279
+ }
280
+ }
281
+
282
+ if (item.badge?.isNotEmpty() == true) {
283
+ val badge = bottomNavigation.getOrCreateBadge(index)
284
+ badge.isVisible = true
285
+ // Set the badge text only if it's different than an empty space to show a small badge.
286
+ // More context: https://github.com/callstackincubator/react-native-bottom-tabs/issues/422
287
+ if (item.badge != " ") {
288
+ badge.text = item.badge
289
+ }
290
+ // Apply badge colors if provided (Material will use its default theme colors otherwise)
291
+ item.badgeBackgroundColor?.let { badge.backgroundColor = it }
292
+ item.badgeTextColor?.let { badge.badgeTextColor = it }
293
+ } else {
294
+ bottomNavigation.removeBadge(index)
295
+ }
296
+ post {
297
+ val itemView = bottomNavigation.findViewById<View>(menuItem.itemId)
298
+ itemView?.let { view ->
299
+ view.setOnLongClickListener {
300
+ onTabLongPressed(menuItem)
301
+ true
302
+ }
303
+ view.setOnClickListener {
304
+ onTabSelected(menuItem)
305
+ }
306
+
307
+ item.testID?.let { testId ->
308
+ view.findViewById<View>(com.google.android.material.R.id.navigation_bar_item_content_container)
309
+ ?.apply {
310
+ tag = testId
311
+ }
312
+ }
313
+ }
314
+ }
315
+ }
316
+ // Update tint colors and text appearance after updating all items.
317
+ post {
318
+ updateTextAppearance()
319
+ updateTintColors()
320
+ }
321
+ }
322
+
323
+ private fun getOrCreateItem(index: Int, title: String): MenuItem {
324
+ return bottomNavigation.menu.findItem(index) ?: bottomNavigation.menu.add(0, index, 0, title)
325
+ }
326
+
327
+ fun setIcons(icons: ReadableArray?) {
328
+ if (icons == null || icons.size() == 0) {
329
+ return
330
+ }
331
+
332
+ for (idx in 0 until icons.size()) {
333
+ val source = icons.getMap(idx)
334
+ val uri = source?.getString("uri")
335
+ if (uri.isNullOrEmpty()) {
336
+ continue
337
+ }
338
+
339
+ val imageSource = ImageSource(context, uri)
340
+ this.iconSources[idx] = imageSource
341
+
342
+ // Update existing item if exists.
343
+ bottomNavigation.menu.findItem(idx)?.let { menuItem ->
344
+ getDrawable(imageSource) {
345
+ menuItem.icon = it
346
+ }
347
+ }
348
+ }
349
+ }
350
+
351
+ fun setLabeled(labeled: Boolean?) {
352
+ this.labeled = labeled
353
+ bottomNavigation.labelVisibilityMode = when (labeled) {
354
+ false -> {
355
+ LABEL_VISIBILITY_UNLABELED
356
+ }
357
+ true -> {
358
+ LABEL_VISIBILITY_LABELED
359
+ }
360
+ else -> {
361
+ LABEL_VISIBILITY_AUTO
362
+ }
363
+ }
364
+ }
365
+
366
+ fun setRippleColor(color: ColorStateList) {
367
+ bottomNavigation.itemRippleColor = color
368
+ }
369
+
370
+ @SuppressLint("CheckResult")
371
+ private fun getDrawable(imageSource: ImageSource, onDrawableReady: (Drawable?) -> Unit) {
372
+ drawableCache[imageSource]?.let {
373
+ onDrawableReady(it)
374
+ return
375
+ }
376
+ val iconSizePx = bottomNavigation.itemIconSize
377
+ val request = ImageRequest.Builder(context)
378
+ .data(imageSource.getUri(context))
379
+ .size(CoilSize(iconSizePx, iconSizePx))
380
+ .scale(Scale.FILL)
381
+ .precision(Precision.EXACT)
382
+ .target { drawable ->
383
+ post {
384
+ val stateDrawable = drawable.asDrawable(context.resources)
385
+ drawableCache[imageSource] = stateDrawable
386
+ onDrawableReady(stateDrawable)
387
+ }
388
+ }
389
+ .listener(
390
+ onError = { _, result ->
391
+ Log.e("RCTTabView", "Error loading image: ${imageSource.uri}", result.throwable)
392
+ }
393
+ )
394
+ .build()
395
+
396
+ imageLoader.enqueue(request)
397
+ }
398
+
399
+ fun setBarTintColor(color: Int?) {
400
+ // Set the color, either using the active background color or a default color.
401
+ val backgroundColor =
402
+ color ?: Utils.getDefaultColorFor(context, android.R.attr.colorPrimary) ?: return
403
+
404
+ // Apply the same color to both active and inactive states
405
+ val colorDrawable = ColorDrawable(backgroundColor)
406
+
407
+ bottomNavigation.itemBackground = colorDrawable
408
+ bottomNavigation.backgroundTintList = ColorStateList.valueOf(backgroundColor)
409
+ hasCustomAppearance = true
410
+ }
411
+
412
+ fun setActiveTintColor(color: Int?) {
413
+ activeTintColor = color
414
+ updateTintColors()
415
+ }
416
+
417
+ fun setInactiveTintColor(color: Int?) {
418
+ inactiveTintColor = color
419
+ updateTintColors()
420
+ }
421
+
422
+ fun setActiveIndicatorColor(color: ColorStateList) {
423
+ bottomNavigation.itemActiveIndicatorColor = color
424
+ }
425
+
426
+ fun setFontSize(size: Int) {
427
+ fontSize = size
428
+ updateTextAppearance()
429
+ }
430
+
431
+ fun setFontFamily(family: String?) {
432
+ fontFamily = family
433
+ updateTextAppearance()
434
+ }
435
+
436
+ fun setFontWeight(weight: String?) {
437
+ val fontWeight = ReactTypefaceUtils.parseFontWeight(weight)
438
+ this.fontWeight = fontWeight
439
+ updateTextAppearance()
440
+ }
441
+
442
+ fun onDropViewInstance() {
443
+ imageLoader.shutdown()
444
+ }
445
+
446
+ private fun updateTextAppearance() {
447
+ // Early return if there is no custom text appearance
448
+ if (fontSize == null && fontFamily == null && fontWeight == null) {
449
+ return
450
+ }
451
+
452
+ val typeface = if (fontFamily != null || fontWeight != null) {
453
+ ReactFontManager.getInstance().getTypeface(
454
+ fontFamily ?: "",
455
+ Utils.getTypefaceStyle(fontWeight),
456
+ context.assets
457
+ )
458
+ } else null
459
+ val size = fontSize?.toFloat()?.takeIf { it > 0 }
460
+
461
+ val menuView = bottomNavigation.getChildAt(0) as? ViewGroup ?: return
462
+ for (i in 0 until menuView.childCount) {
463
+ val item = menuView.getChildAt(i)
464
+ val largeLabel =
465
+ item.findViewById<TextView>(com.google.android.material.R.id.navigation_bar_item_large_label_view)
466
+ val smallLabel =
467
+ item.findViewById<TextView>(com.google.android.material.R.id.navigation_bar_item_small_label_view)
468
+
469
+ listOf(largeLabel, smallLabel).forEach { label ->
470
+ label?.apply {
471
+ size?.let { size ->
472
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, size)
473
+ }
474
+ typeface?.let { setTypeface(it) }
475
+ }
476
+ }
477
+ }
478
+ }
479
+
480
+ private fun emitHapticFeedback(feedbackConstants: Int) {
481
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && hapticFeedbackEnabled) {
482
+ this.performHapticFeedback(feedbackConstants)
483
+ }
484
+ }
485
+
486
+ private fun updateTintColors() {
487
+ // First let's check current item color.
488
+ val currentItemTintColor = items.firstOrNull { it.key == selectedItem }?.activeTintColor
489
+
490
+ // getDefaultColor will always return a valid color but to satisfy the compiler we need to check for null
491
+ val colorPrimary = currentItemTintColor ?: activeTintColor ?: Utils.getDefaultColorFor(
492
+ context,
493
+ android.R.attr.colorPrimary
494
+ ) ?: return
495
+ val colorSecondary =
496
+ inactiveTintColor ?: Utils.getDefaultColorFor(context, android.R.attr.textColorSecondary)
497
+ ?: return
498
+ val states = arrayOf(uncheckedStateSet, checkedStateSet)
499
+ val colors = intArrayOf(colorSecondary, colorPrimary)
500
+
501
+ ColorStateList(states, colors).apply {
502
+ this@ReactBottomNavigationView.bottomNavigation.itemTextColor = this
503
+ this@ReactBottomNavigationView.bottomNavigation.itemIconTintList = this
504
+ }
505
+ }
506
+
507
+ override fun onConfigurationChanged(newConfig: Configuration?) {
508
+ super.onConfigurationChanged(newConfig)
509
+ if (uiModeConfiguration == newConfig?.uiMode || hasCustomAppearance) {
510
+ return
511
+ }
512
+
513
+ // User has hidden the bottom navigation bar, don't re-attach it.
514
+ if (bottomNavigation.visibility == GONE) {
515
+ return
516
+ }
517
+
518
+ // If appearance wasn't changed re-create the bottom navigation view when configuration changes.
519
+ // React Native opts out ouf Activity re-creation when configuration changes, this workarounds that.
520
+ // We also opt-out of this recreation when custom styles are used.
521
+ removeView(bottomNavigation)
522
+ bottomNavigation = ExtendedBottomNavigationView(context)
523
+ addView(bottomNavigation)
524
+ updateItems(items)
525
+ setLabeled(this.labeled)
526
+ this.selectedItem?.let { setSelectedItem(it) }
527
+ uiModeConfiguration = newConfig?.uiMode ?: uiModeConfiguration
528
+ }
529
+ }