@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.
- package/android/build.gradle +119 -0
- package/android/gradle.properties +4 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/AndroidManifestNew.xml +2 -0
- package/android/src/main/java/com/rcttabview/ImageSource.kt +86 -0
- package/android/src/main/java/com/rcttabview/RCTTabView.kt +529 -0
- package/android/src/main/java/com/rcttabview/RCTTabViewManager.kt +204 -0
- package/android/src/main/java/com/rcttabview/RCTTabViewPackage.kt +16 -0
- package/android/src/main/java/com/rcttabview/TabInfo.kt +12 -0
- package/android/src/main/java/com/rcttabview/Utils.kt +31 -0
- package/android/src/main/java/com/rcttabview/events/OnNativeLayoutEvent.kt +20 -0
- package/android/src/main/java/com/rcttabview/events/OnTabBarMeasuredEvent.kt +19 -0
- package/android/src/main/java/com/rcttabview/events/PageSelectedEvent.kt +21 -0
- package/android/src/main/java/com/rcttabview/events/TabLongPressedEvent.kt +19 -0
- package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewComponentDescriptor.h +32 -0
- package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.cpp +18 -0
- package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.h +35 -0
- package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewState.cpp +15 -0
- package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewState.h +25 -0
- package/ios/Extensions.swift +46 -0
- package/ios/RCTBottomAccessoryComponentView.h +12 -0
- package/ios/RCTBottomAccessoryComponentView.mm +67 -0
- package/ios/RCTBottomAccessoryContainerView.swift +51 -0
- package/ios/RCTTabViewComponentView.h +12 -0
- package/ios/RCTTabViewComponentView.mm +325 -0
- package/ios/RCTTabViewContainerView.swift +768 -0
- package/ios/RCTTabViewLog.h +7 -0
- package/ios/RCTTabViewLog.m +32 -0
- package/ios/SVG/CoreSVG.h +13 -0
- package/ios/SVG/CoreSVG.mm +177 -0
- package/ios/SVG/SvgDecoder.h +10 -0
- package/ios/SVG/SvgDecoder.mm +32 -0
- package/ios/TabBarFontSize.swift +55 -0
- package/lib/module/BottomAccessoryView.js +45 -0
- package/lib/module/BottomAccessoryView.js.map +1 -0
- package/lib/module/BottomAccessoryViewNativeComponent.ts +27 -0
- package/lib/module/DelayedFreeze.js +26 -0
- package/lib/module/DelayedFreeze.js.map +1 -0
- package/lib/module/NativeSVGDecoder.js +5 -0
- package/lib/module/NativeSVGDecoder.js.map +1 -0
- package/lib/module/SceneMap.js +28 -0
- package/lib/module/SceneMap.js.map +1 -0
- package/lib/module/TabView.js +263 -0
- package/lib/module/TabView.js.map +1 -0
- package/lib/module/TabViewNativeComponent.ts +68 -0
- package/lib/module/codegen-types.d.js +2 -0
- package/lib/module/codegen-types.d.js.map +1 -0
- package/lib/module/index.js +20 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utils/BottomTabBarHeightContext.js +5 -0
- package/lib/module/utils/BottomTabBarHeightContext.js.map +1 -0
- package/lib/module/utils/useBottomTabBarHeight.js +12 -0
- package/lib/module/utils/useBottomTabBarHeight.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/BottomAccessoryView.d.ts +8 -0
- package/lib/typescript/src/BottomAccessoryView.d.ts.map +1 -0
- package/lib/typescript/src/BottomAccessoryViewNativeComponent.d.ts +16 -0
- package/lib/typescript/src/BottomAccessoryViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/DelayedFreeze.d.ts +8 -0
- package/lib/typescript/src/DelayedFreeze.d.ts.map +1 -0
- package/lib/typescript/src/NativeSVGDecoder.d.ts +6 -0
- package/lib/typescript/src/NativeSVGDecoder.d.ts.map +1 -0
- package/lib/typescript/src/SceneMap.d.ts +10 -0
- package/lib/typescript/src/SceneMap.d.ts.map +1 -0
- package/lib/typescript/src/TabView.d.ts +178 -0
- package/lib/typescript/src/TabView.d.ts.map +1 -0
- package/lib/typescript/src/TabViewNativeComponent.d.ts +55 -0
- package/lib/typescript/src/TabViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +16 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +29 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/utils/BottomTabBarHeightContext.d.ts +3 -0
- package/lib/typescript/src/utils/BottomTabBarHeightContext.d.ts.map +1 -0
- package/lib/typescript/src/utils/useBottomTabBarHeight.d.ts +2 -0
- package/lib/typescript/src/utils/useBottomTabBarHeight.d.ts.map +1 -0
- package/package.json +114 -0
- package/react-native-tab-view.podspec +36 -0
- package/react-native.config.js +13 -0
- package/src/BottomAccessoryView.tsx +58 -0
- package/src/BottomAccessoryViewNativeComponent.ts +27 -0
- package/src/DelayedFreeze.tsx +27 -0
- package/src/NativeSVGDecoder.ts +5 -0
- package/src/SceneMap.tsx +34 -0
- package/src/TabView.tsx +466 -0
- package/src/TabViewNativeComponent.ts +68 -0
- package/src/codegen-types.d.ts +28 -0
- package/src/index.tsx +18 -0
- package/src/types.ts +31 -0
- package/src/utils/BottomTabBarHeightContext.ts +5 -0
- 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
|
+
}
|