Skip to content

Commit d40bdac

Browse files
committed
fix: handle all image sources
1 parent 2eebe0a commit d40bdac

File tree

2 files changed

+102
-41
lines changed

2 files changed

+102
-41
lines changed
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 & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import android.content.res.ColorStateList
66
import android.graphics.Typeface
77
import android.graphics.drawable.ColorDrawable
88
import android.graphics.drawable.Drawable
9-
import android.net.Uri
109
import android.os.Build
1110
import android.util.Log
1211
import android.util.TypedValue
@@ -24,20 +23,18 @@ import com.facebook.react.bridge.ReadableArray
2423
import com.facebook.react.bridge.WritableMap
2524
import com.facebook.react.common.assets.ReactFontManager
2625
import com.facebook.react.modules.core.ReactChoreographer
27-
import com.facebook.react.views.imagehelper.ImageSource
2826
import com.facebook.react.views.text.ReactTypefaceUtils
2927
import com.google.android.material.bottomnavigation.BottomNavigationView
3028
import coil3.request.ImageRequest
3129
import coil3.svg.SvgDecoder
3230

3331

3432
class ReactBottomNavigationView(context: Context) : BottomNavigationView(context) {
35-
private val icons: MutableMap<Int, ImageSource> = mutableMapOf()
33+
private val iconSources: MutableMap<Int, ImageSource> = mutableMapOf()
3634
private var isLayoutEnqueued = false
3735
var items: MutableList<TabInfo>? = null
3836
var onTabSelectedListener: ((WritableMap) -> Unit)? = null
3937
var onTabLongPressedListener: ((WritableMap) -> Unit)? = null
40-
private var isAnimating = false
4138
private var activeTintColor: Int? = null
4239
private var inactiveTintColor: Int? = null
4340
private val checkedStateSet = intArrayOf(android.R.attr.state_checked)
@@ -92,7 +89,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
9289

9390
private fun onTabSelected(item: MenuItem) {
9491
if (isLayoutEnqueued) {
95-
return;
92+
return
9693
}
9794
val selectedItem = items?.first { it.title == item.title }
9895
selectedItem?.let {
@@ -109,8 +106,8 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
109106
items.forEachIndexed { index, item ->
110107
val menuItem = getOrCreateItem(index, item.title)
111108
menuItem.isVisible = !item.hidden
112-
if (icons.containsKey(index)) {
113-
getDrawable(icons[index]!!) {
109+
if (iconSources.containsKey(index)) {
110+
getDrawable(iconSources[index]!!) {
114111
menuItem.icon = it
115112
}
116113
}
@@ -151,12 +148,9 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
151148
if (uri.isNullOrEmpty()) {
152149
continue
153150
}
154-
val imageSource =
155-
ImageSource(
156-
context,
157-
uri
158-
)
159-
this.icons[idx] = imageSource
151+
152+
val imageSource = ImageSource(context, uri)
153+
this.iconSources[idx] = imageSource
160154

161155
// Update existing item if exists.
162156
menu.findItem(idx)?.let { menuItem ->
@@ -181,27 +175,10 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
181175
itemRippleColor = color
182176
}
183177

184-
private fun formatUri(uri: Uri): Uri {
185-
return when (uri.scheme) {
186-
"res" -> {
187-
val uriString = uri.toString()
188-
val parts = uriString.split(":/")
189-
190-
if (parts.size > 1) {
191-
val resourceId = parts[1].toIntOrNull()
192-
Uri.parse("android.resource://${context.packageName}/${resourceId}")
193-
} else {
194-
uri
195-
}
196-
}
197-
else -> uri
198-
}
199-
}
200-
201178
@SuppressLint("CheckResult")
202179
private fun getDrawable(imageSource: ImageSource, onDrawableReady: (Drawable?) -> Unit) {
203180
val request = ImageRequest.Builder(context)
204-
.data(formatUri(imageSource.uri))
181+
.data(imageSource.getUri(context))
205182
.target { drawable ->
206183
post { onDrawableReady(drawable.asDrawable(context.resources)) }
207184
}
@@ -215,11 +192,6 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
215192
imageLoader.enqueue(request)
216193
}
217194

218-
override fun onDetachedFromWindow() {
219-
super.onDetachedFromWindow()
220-
isAnimating = false
221-
}
222-
223195
fun setBarTintColor(color: Int?) {
224196
// Set the color, either using the active background color or a default color.
225197
val backgroundColor = color ?: getDefaultColorFor(android.R.attr.colorPrimary) ?: return
@@ -259,10 +231,10 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
259231
updateTextAppearance()
260232
}
261233

262-
fun setFontWeight(weight: String?) {
263-
val fontWeight = ReactTypefaceUtils.parseFontWeight(weight)
264-
this.fontWeight = fontWeight
265-
updateTextAppearance()
234+
fun setFontWeight(weight: String?) {
235+
val fontWeight = ReactTypefaceUtils.parseFontWeight(weight)
236+
this.fontWeight = fontWeight
237+
updateTextAppearance()
266238
}
267239

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

310-
// 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
311283
val colorPrimary = currentItemTintColor ?: activeTintColor ?: getDefaultColorFor(android.R.attr.colorPrimary) ?: return
312284
val colorSecondary =
313285
inactiveTintColor ?: getDefaultColorFor(android.R.attr.textColorSecondary) ?: return
@@ -331,3 +303,6 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
331303
return baseColor.defaultColor
332304
}
333305
}
306+
307+
308+

0 commit comments

Comments
 (0)