Skip to content

Commit c31a00f

Browse files
fix(android): Handle tabBarIcon sources (#175)
* fix: android tabBarIcon not visible when build to apk * fix: handle all image sources * Create orange-pandas-exercise.md --------- Co-authored-by: Ethan <ethan@zolplay.com>
1 parent fc096de commit c31a00f

File tree

3 files changed

+107
-23
lines changed

3 files changed

+107
-23
lines changed

.changeset/orange-pandas-exercise.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-native-bottom-tabs": patch
3+
---
4+
5+
fix(android): handle tabBarIcon sources in release mode
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package com.rcttabview
2+
3+
import android.annotation.SuppressLint
4+
import android.content.Context
5+
import android.net.Uri
6+
import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper
7+
import java.util.Locale
8+
9+
data class ImageSource(
10+
val context: Context,
11+
val uri: String? = null,
12+
) {
13+
private fun isLocalResourceUri(uri: Uri?) = uri?.scheme?.startsWith("res") ?: false
14+
15+
fun getUri(context: Context): Uri? {
16+
val uri = computeUri(context)
17+
18+
if (isLocalResourceUri(uri)) {
19+
return Uri.parse(
20+
uri!!.toString().replace("res:/", "android.resource://" + context.packageName + "/")
21+
)
22+
}
23+
24+
return uri
25+
}
26+
27+
private fun computeUri(context: Context): Uri? {
28+
val stringUri = uri ?: return null
29+
return try {
30+
val uri: Uri = Uri.parse(stringUri)
31+
// Verify scheme is set, so that relative uri (used by static resources) are not handled.
32+
if (uri.scheme == null) {
33+
computeLocalUri(stringUri, context)
34+
} else {
35+
uri
36+
}
37+
} catch (e: Exception) {
38+
computeLocalUri(stringUri, context)
39+
}
40+
}
41+
42+
private fun computeLocalUri(stringUri: String, context: Context): Uri? {
43+
return ResourceIdHelper.getResourceUri(context, stringUri)
44+
}
45+
}
46+
47+
// Taken from https://github.com/expo/expo/blob/sdk-52/packages/expo-image/android/src/main/java/expo/modules/image/ResourceIdHelper.kt
48+
object ResourceIdHelper {
49+
private val idMap = mutableMapOf<String, Int>()
50+
51+
@SuppressLint("DiscouragedApi")
52+
private fun getResourceRawId(context: Context, name: String): Int {
53+
if (name.isEmpty()) {
54+
return -1
55+
}
56+
57+
val normalizedName = name.lowercase(Locale.ROOT).replace("-", "_")
58+
synchronized(this) {
59+
val id = idMap[normalizedName]
60+
if (id != null) {
61+
return id
62+
}
63+
64+
return context
65+
.resources
66+
.getIdentifier(normalizedName, "raw", context.packageName)
67+
.also {
68+
idMap[normalizedName] = it
69+
}
70+
}
71+
}
72+
73+
fun getResourceUri(context: Context, name: String): Uri? {
74+
val drawableUri = ResourceDrawableIdHelper.instance.getResourceDrawableUri(context, name)
75+
if (drawableUri != Uri.EMPTY) {
76+
return drawableUri
77+
}
78+
79+
val resId = getResourceRawId(context, name)
80+
return if (resId > 0) {
81+
Uri.Builder().scheme("res").path(resId.toString()).build()
82+
} else {
83+
null
84+
}
85+
}
86+
}

packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,18 @@ import com.facebook.react.bridge.ReadableArray
2323
import com.facebook.react.bridge.WritableMap
2424
import com.facebook.react.common.assets.ReactFontManager
2525
import com.facebook.react.modules.core.ReactChoreographer
26-
import com.facebook.react.views.imagehelper.ImageSource
2726
import com.facebook.react.views.text.ReactTypefaceUtils
2827
import com.google.android.material.bottomnavigation.BottomNavigationView
2928
import coil3.request.ImageRequest
3029
import coil3.svg.SvgDecoder
3130

3231

3332
class ReactBottomNavigationView(context: Context) : BottomNavigationView(context) {
34-
private val icons: MutableMap<Int, ImageSource> = mutableMapOf()
33+
private val iconSources: MutableMap<Int, ImageSource> = mutableMapOf()
3534
private var isLayoutEnqueued = false
3635
var items: MutableList<TabInfo>? = null
3736
var onTabSelectedListener: ((WritableMap) -> Unit)? = null
3837
var onTabLongPressedListener: ((WritableMap) -> Unit)? = null
39-
private var isAnimating = false
4038
private var activeTintColor: Int? = null
4139
private var inactiveTintColor: Int? = null
4240
private val checkedStateSet = intArrayOf(android.R.attr.state_checked)
@@ -91,7 +89,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
9189

9290
private fun onTabSelected(item: MenuItem) {
9391
if (isLayoutEnqueued) {
94-
return;
92+
return
9593
}
9694
val selectedItem = items?.first { it.title == item.title }
9795
selectedItem?.let {
@@ -108,8 +106,8 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
108106
items.forEachIndexed { index, item ->
109107
val menuItem = getOrCreateItem(index, item.title)
110108
menuItem.isVisible = !item.hidden
111-
if (icons.containsKey(index)) {
112-
getDrawable(icons[index]!!) {
109+
if (iconSources.containsKey(index)) {
110+
getDrawable(iconSources[index]!!) {
113111
menuItem.icon = it
114112
}
115113
}
@@ -150,12 +148,9 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
150148
if (uri.isNullOrEmpty()) {
151149
continue
152150
}
153-
val imageSource =
154-
ImageSource(
155-
context,
156-
uri
157-
)
158-
this.icons[idx] = imageSource
151+
152+
val imageSource = ImageSource(context, uri)
153+
this.iconSources[idx] = imageSource
159154

160155
// Update existing item if exists.
161156
menu.findItem(idx)?.let { menuItem ->
@@ -183,7 +178,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
183178
@SuppressLint("CheckResult")
184179
private fun getDrawable(imageSource: ImageSource, onDrawableReady: (Drawable?) -> Unit) {
185180
val request = ImageRequest.Builder(context)
186-
.data(imageSource.uri)
181+
.data(imageSource.getUri(context))
187182
.target { drawable ->
188183
post { onDrawableReady(drawable.asDrawable(context.resources)) }
189184
}
@@ -197,11 +192,6 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
197192
imageLoader.enqueue(request)
198193
}
199194

200-
override fun onDetachedFromWindow() {
201-
super.onDetachedFromWindow()
202-
isAnimating = false
203-
}
204-
205195
fun setBarTintColor(color: Int?) {
206196
// Set the color, either using the active background color or a default color.
207197
val backgroundColor = color ?: getDefaultColorFor(android.R.attr.colorPrimary) ?: return
@@ -241,10 +231,10 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
241231
updateTextAppearance()
242232
}
243233

244-
fun setFontWeight(weight: String?) {
245-
val fontWeight = ReactTypefaceUtils.parseFontWeight(weight)
246-
this.fontWeight = fontWeight
247-
updateTextAppearance()
234+
fun setFontWeight(weight: String?) {
235+
val fontWeight = ReactTypefaceUtils.parseFontWeight(weight)
236+
this.fontWeight = fontWeight
237+
updateTextAppearance()
248238
}
249239

250240
private fun getTypefaceStyle(weight: Int?) = when (weight) {
@@ -289,7 +279,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
289279
// First let's check current item color.
290280
val currentItemTintColor = items?.find { it.title == item?.title }?.activeTintColor
291281

292-
// getDeaultColor will always return a valid color but to satisfy the compiler we need to check for null
282+
// getDefaultColor will always return a valid color but to satisfy the compiler we need to check for null
293283
val colorPrimary = currentItemTintColor ?: activeTintColor ?: getDefaultColorFor(android.R.attr.colorPrimary) ?: return
294284
val colorSecondary =
295285
inactiveTintColor ?: getDefaultColorFor(android.R.attr.textColorSecondary) ?: return
@@ -313,3 +303,6 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
313303
return baseColor.defaultColor
314304
}
315305
}
306+
307+
308+

0 commit comments

Comments
 (0)