From c31a00f4bf9ca098bc8f4e34aed40e29b443d60c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwa=C5=9Bniewski?= Date: Tue, 3 Dec 2024 11:57:27 +0100 Subject: [PATCH] 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 --- .changeset/orange-pandas-exercise.md | 5 ++ .../main/java/com/rcttabview/ImageSource.kt | 86 +++++++++++++++++++ .../main/java/com/rcttabview/RCTTabView.kt | 39 ++++----- 3 files changed, 107 insertions(+), 23 deletions(-) create mode 100644 .changeset/orange-pandas-exercise.md create mode 100644 packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/ImageSource.kt diff --git a/.changeset/orange-pandas-exercise.md b/.changeset/orange-pandas-exercise.md new file mode 100644 index 0000000..6483a99 --- /dev/null +++ b/.changeset/orange-pandas-exercise.md @@ -0,0 +1,5 @@ +--- +"react-native-bottom-tabs": patch +--- + +fix(android): handle tabBarIcon sources in release mode diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/ImageSource.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/ImageSource.kt new file mode 100644 index 0000000..c8fd84d --- /dev/null +++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/ImageSource.kt @@ -0,0 +1,86 @@ +package com.rcttabview + +import android.annotation.SuppressLint +import android.content.Context +import android.net.Uri +import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper +import java.util.Locale + +data class ImageSource( + val context: Context, + val uri: String? = null, +) { + private fun isLocalResourceUri(uri: Uri?) = uri?.scheme?.startsWith("res") ?: false + + fun getUri(context: Context): Uri? { + val uri = computeUri(context) + + if (isLocalResourceUri(uri)) { + return Uri.parse( + uri!!.toString().replace("res:/", "android.resource://" + context.packageName + "/") + ) + } + + return uri + } + + private fun computeUri(context: Context): Uri? { + val stringUri = uri ?: return null + return try { + val uri: Uri = Uri.parse(stringUri) + // Verify scheme is set, so that relative uri (used by static resources) are not handled. + if (uri.scheme == null) { + computeLocalUri(stringUri, context) + } else { + uri + } + } catch (e: Exception) { + computeLocalUri(stringUri, context) + } + } + + private fun computeLocalUri(stringUri: String, context: Context): Uri? { + return ResourceIdHelper.getResourceUri(context, stringUri) + } +} + +// Taken from https://github.com/expo/expo/blob/sdk-52/packages/expo-image/android/src/main/java/expo/modules/image/ResourceIdHelper.kt +object ResourceIdHelper { + private val idMap = mutableMapOf() + + @SuppressLint("DiscouragedApi") + private fun getResourceRawId(context: Context, name: String): Int { + if (name.isEmpty()) { + return -1 + } + + val normalizedName = name.lowercase(Locale.ROOT).replace("-", "_") + synchronized(this) { + val id = idMap[normalizedName] + if (id != null) { + return id + } + + return context + .resources + .getIdentifier(normalizedName, "raw", context.packageName) + .also { + idMap[normalizedName] = it + } + } + } + + fun getResourceUri(context: Context, name: String): Uri? { + val drawableUri = ResourceDrawableIdHelper.instance.getResourceDrawableUri(context, name) + if (drawableUri != Uri.EMPTY) { + return drawableUri + } + + val resId = getResourceRawId(context, name) + return if (resId > 0) { + Uri.Builder().scheme("res").path(resId.toString()).build() + } else { + null + } + } +} diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt index b23bce8..65e7b4f 100644 --- a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt +++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt @@ -23,7 +23,6 @@ import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.WritableMap import com.facebook.react.common.assets.ReactFontManager import com.facebook.react.modules.core.ReactChoreographer -import com.facebook.react.views.imagehelper.ImageSource import com.facebook.react.views.text.ReactTypefaceUtils import com.google.android.material.bottomnavigation.BottomNavigationView import coil3.request.ImageRequest @@ -31,12 +30,11 @@ import coil3.svg.SvgDecoder class ReactBottomNavigationView(context: Context) : BottomNavigationView(context) { - private val icons: MutableMap = mutableMapOf() + private val iconSources: MutableMap = mutableMapOf() private var isLayoutEnqueued = false var items: MutableList? = null var onTabSelectedListener: ((WritableMap) -> Unit)? = null var onTabLongPressedListener: ((WritableMap) -> Unit)? = null - private var isAnimating = false private var activeTintColor: Int? = null private var inactiveTintColor: Int? = null private val checkedStateSet = intArrayOf(android.R.attr.state_checked) @@ -91,7 +89,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context private fun onTabSelected(item: MenuItem) { if (isLayoutEnqueued) { - return; + return } val selectedItem = items?.first { it.title == item.title } selectedItem?.let { @@ -108,8 +106,8 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context items.forEachIndexed { index, item -> val menuItem = getOrCreateItem(index, item.title) menuItem.isVisible = !item.hidden - if (icons.containsKey(index)) { - getDrawable(icons[index]!!) { + if (iconSources.containsKey(index)) { + getDrawable(iconSources[index]!!) { menuItem.icon = it } } @@ -150,12 +148,9 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context if (uri.isNullOrEmpty()) { continue } - val imageSource = - ImageSource( - context, - uri - ) - this.icons[idx] = imageSource + + val imageSource = ImageSource(context, uri) + this.iconSources[idx] = imageSource // Update existing item if exists. menu.findItem(idx)?.let { menuItem -> @@ -183,7 +178,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context @SuppressLint("CheckResult") private fun getDrawable(imageSource: ImageSource, onDrawableReady: (Drawable?) -> Unit) { val request = ImageRequest.Builder(context) - .data(imageSource.uri) + .data(imageSource.getUri(context)) .target { drawable -> post { onDrawableReady(drawable.asDrawable(context.resources)) } } @@ -197,11 +192,6 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context imageLoader.enqueue(request) } - override fun onDetachedFromWindow() { - super.onDetachedFromWindow() - isAnimating = false - } - fun setBarTintColor(color: Int?) { // Set the color, either using the active background color or a default color. val backgroundColor = color ?: getDefaultColorFor(android.R.attr.colorPrimary) ?: return @@ -241,10 +231,10 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context updateTextAppearance() } - fun setFontWeight(weight: String?) { - val fontWeight = ReactTypefaceUtils.parseFontWeight(weight) - this.fontWeight = fontWeight - updateTextAppearance() + fun setFontWeight(weight: String?) { + val fontWeight = ReactTypefaceUtils.parseFontWeight(weight) + this.fontWeight = fontWeight + updateTextAppearance() } private fun getTypefaceStyle(weight: Int?) = when (weight) { @@ -289,7 +279,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context // First let's check current item color. val currentItemTintColor = items?.find { it.title == item?.title }?.activeTintColor - // getDeaultColor will always return a valid color but to satisfy the compiler we need to check for null + // getDefaultColor will always return a valid color but to satisfy the compiler we need to check for null val colorPrimary = currentItemTintColor ?: activeTintColor ?: getDefaultColorFor(android.R.attr.colorPrimary) ?: return val colorSecondary = inactiveTintColor ?: getDefaultColorFor(android.R.attr.textColorSecondary) ?: return @@ -313,3 +303,6 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context return baseColor.defaultColor } } + + +