Skip to content

Commit 3b66be2

Browse files
authored
feat: Handle custom icons (#8)
* feat: add material bottom tabs reference example * feat: implement custom image handling on iOS * feat: implement custom image handling on Android * fix: native iOS build * fix: support remote images for android * feat: handle SF Symbols * feat: add sfsymbols * fix: top safe area * attempt to fix CI
1 parent f121778 commit 3b66be2

24 files changed

+599
-177
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,8 @@ jobs:
147147
- name: Install cocoapods
148148
if: env.turbo_cache_hit != 1 && steps.cocoapods-cache.outputs.cache-hit != 'true'
149149
run: |
150-
cd example/ios
151-
pod install
150+
cd example
151+
pod install --project-directory=ios
152152
env:
153153
NO_FLIPPER: 1
154154

android/src/main/java/com/rcttabview/RCTTabView.kt

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,27 @@
11
package com.rcttabview
22

33
import android.content.Context
4+
import android.graphics.drawable.BitmapDrawable
5+
import android.graphics.drawable.Drawable
6+
import android.net.Uri
47
import android.view.Choreographer
58
import android.view.MenuItem
6-
import androidx.appcompat.content.res.AppCompatResources
9+
import com.facebook.common.references.CloseableReference
10+
import com.facebook.datasource.DataSources
11+
import com.facebook.drawee.backends.pipeline.Fresco
12+
import com.facebook.imagepipeline.image.CloseableBitmap
13+
import com.facebook.imagepipeline.request.ImageRequestBuilder
714
import com.facebook.react.bridge.Arguments
15+
import com.facebook.react.bridge.ReadableArray
816
import com.facebook.react.bridge.WritableMap
17+
import com.facebook.react.views.imagehelper.ImageSource
18+
import com.facebook.react.views.imagehelper.ImageSource.Companion.getTransparentBitmapImageSource
919
import com.google.android.material.bottomnavigation.BottomNavigationView
1020

21+
1122
class ReactBottomNavigationView(context: Context) : BottomNavigationView(context) {
1223
private val ANIMATION_DURATION: Long = 300
24+
private val icons: MutableMap<Int, ImageSource> = mutableMapOf()
1325

1426
var items: MutableList<TabInfo>? = null
1527
var onTabSelectedListener: ((WritableMap) -> Unit)? = null
@@ -62,17 +74,10 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
6274

6375
fun updateItems(items: MutableList<TabInfo>) {
6476
this.items = items
65-
// TODO: This doesn't work with hot reload. It clears all menu items
66-
menu.clear()
6777
items.forEachIndexed {index, item ->
68-
val menuItem = menu.add(0, index, 0, item.title)
69-
val iconResourceId = resources.getIdentifier(
70-
item.icon, "drawable", context.packageName
71-
)
72-
if (iconResourceId != 0) {
73-
menuItem.icon = AppCompatResources.getDrawable(context, iconResourceId)
74-
} else {
75-
menuItem.setIcon(android.R.drawable.btn_star) // fallback icon
78+
val menuItem = getOrCreateItem(index, item.title)
79+
if (icons.containsKey(index)) {
80+
menuItem.icon = getDrawable(icons[index]!!)
7681
}
7782
if (item.badge.isNotEmpty()) {
7883
val badge = this.getOrCreateBadge(index)
@@ -84,6 +89,47 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
8489
}
8590
}
8691

92+
private fun getOrCreateItem(index: Int, title: String): MenuItem {
93+
return menu.findItem(index) ?: menu.add(0, index, 0, title)
94+
}
95+
96+
fun setIcons(icons: ReadableArray?) {
97+
if (icons == null || icons.size() == 0) {
98+
return
99+
}
100+
101+
for (idx in 0 until icons.size()) {
102+
val source = icons.getMap(idx)
103+
var imageSource =
104+
ImageSource(
105+
context,
106+
source.getString("uri")
107+
)
108+
if (Uri.EMPTY == imageSource.uri) {
109+
imageSource = getTransparentBitmapImageSource(context)
110+
}
111+
this.icons[idx] = imageSource
112+
113+
// Update existing item if exists.
114+
menu.findItem(idx)?.let { menuItem ->
115+
menuItem.icon = getDrawable(imageSource)
116+
}
117+
}
118+
}
119+
120+
private fun getDrawable(imageSource: ImageSource): Drawable {
121+
// TODO: Check if this can be done using some built-in React Native class
122+
val imageRequest = ImageRequestBuilder.newBuilderWithSource(imageSource.uri).build()
123+
val dataSource = Fresco.getImagePipeline().fetchDecodedImage(imageRequest, context)
124+
val result = DataSources.waitForFinalResult(dataSource) as CloseableReference<CloseableBitmap>
125+
val bitmap = result.get().underlyingBitmap
126+
127+
CloseableReference.closeSafely(result)
128+
dataSource.close()
129+
130+
return BitmapDrawable(resources, bitmap)
131+
}
132+
87133
// Fixes issues with BottomNavigationView children layouting.
88134
private fun measureAndLayout() {
89135
measure(

android/src/main/java/com/rcttabview/RCTTabViewViewManager.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import com.facebook.yoga.YogaNode
1818

1919
data class TabInfo(
2020
val key: String,
21-
val icon: String,
2221
val title: String,
2322
val badge: String
2423
)
@@ -40,7 +39,6 @@ class RCTTabViewViewManager :
4039
itemsArray.add(
4140
TabInfo(
4241
key = item.getString("key") ?: "",
43-
icon = item.getString("icon") ?: "",
4442
title = item.getString("title") ?: "",
4543
badge = item.getString("badge") ?: ""
4644
)
@@ -57,6 +55,11 @@ class RCTTabViewViewManager :
5755
}
5856
}
5957

58+
@ReactProp(name = "icons")
59+
fun setIcons(view: ReactBottomNavigationView, icons: ReadableArray?) {
60+
view.setIcons(icons)
61+
}
62+
6063
public override fun createViewInstance(context: ThemedReactContext): ReactBottomNavigationView {
6164
eventDispatcher = context.getNativeModule(UIManagerModule::class.java)!!.eventDispatcher
6265
val view = ReactBottomNavigationView(context)
853 Bytes
Loading

example/assets/icons/chat_dark.png

789 Bytes
Loading

example/assets/icons/grid_dark.png

710 Bytes
Loading
1.05 KB
Loading

example/ios/Podfile.lock

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1774,15 +1774,15 @@ SPEC CHECKSUMS:
17741774
React-CoreModules: 2d68c251bc4080028f2835fa47504e8f20669a21
17751775
React-cxxreact: bb0dc212b515d6dba6c6ddc4034584e148857db9
17761776
React-debug: fd0ed8ecd5f8a23c7daf5ceaca8aa722a4d083fd
1777-
React-defaultsnativemodule: 371dc516e5020f8b87f1d32f8fa6872cafcc2081
1778-
React-domnativemodule: 5d1288b9b8666b818a1004b56a03befc00eb5698
1777+
React-defaultsnativemodule: 0d824306a15dd80e2bea12f4079fbeff9712b301
1778+
React-domnativemodule: 195491d7c1725befd636f84c67bf229203fc7d07
17791779
React-Fabric: c12ce848f72cba42fb9e97a73a7c99abc6353f23
17801780
React-FabricComponents: 7813d5575c8ea2cda0fef9be4ff9d10987cba512
17811781
React-FabricImage: c511a5d612479cb4606edf3557c071956c8735f6
17821782
React-featureflags: cf78861db9318ae29982fa8953c92d31b276c9ac
1783-
React-featureflagsnativemodule: e774cf495486b0e2a8b324568051d6b4c722fa93
1783+
React-featureflagsnativemodule: 54f6decea27c187c2127e3669a7f5bf2e145e637
17841784
React-graphics: 7572851bca7242416b648c45d6af87d93d29281e
1785-
React-idlecallbacksnativemodule: d2009bad67ef232a0ee586f53193f37823e81ef1
1785+
React-idlecallbacksnativemodule: 7d21b0e071c3e02bcc897d2c3db51319642dd466
17861786
React-ImageManager: aedf54d34d4475c66f4c3da6b8359b95bee904e4
17871787
React-jsc: 92ac98e0e03ee54fdaa4ac3936285a4fdb166fab
17881788
React-jserrorhandler: 0c8949672a00f2a502c767350e591e3ec3d82fb3
@@ -1792,8 +1792,8 @@ SPEC CHECKSUMS:
17921792
React-jsitracing: 3935b092f85bb1e53b8cf8a00f572413648af46b
17931793
React-logger: 4072f39df335ca443932e0ccece41fbeb5ca8404
17941794
React-Mapbuffer: 714f2fae68edcabfc332b754e9fbaa8cfc68fdd4
1795-
React-microtasksnativemodule: 987cf7e0e0e7129250a48b807e70d3b906c726cf
1796-
react-native-bottom-tabs: 894d1fb8fc4e6d525b2da35e83e00e18c420cdf2
1795+
React-microtasksnativemodule: 618b64238e43ef3154079f193aa6649e5320ae19
1796+
react-native-bottom-tabs: 5662b5e3b5968bec6258b9d6f1a0a834bd3f7553
17971797
react-native-safe-area-context: 851c62c48dce80ccaa5637b6aa5991a1bc36eca9
17981798
React-nativeconfig: 4a9543185905fe41014c06776bf126083795aed9
17991799
React-NativeModulesApple: 651670a799672bd54469f2981d91493dda361ddf
@@ -1820,11 +1820,11 @@ SPEC CHECKSUMS:
18201820
React-utils: b2baee839fb869f732d617b97dcfa384b4b4fdb3
18211821
ReactCodegen: f177b8fd67788c5c6ff45a39c7482c5f8d77ace6
18221822
ReactCommon: 627bd3192ef01a351e804e9709673d3741d38fec
1823-
ReactNativeHost: 99c0ffb175cd69de2ac9a70892cd22dac65ea79d
1823+
ReactNativeHost: 62249d6e1e42a969159946c035c1cd3f4b1035dd
18241824
ReactTestApp-DevSupport: b7cd76a3aeee6167f5e14d82f09685059152c426
18251825
ReactTestApp-Resources: 7db90c026cccdf40cfa495705ad436ccc4d64154
1826-
RNGestureHandler: 18b9b5d65c77c4744a640f69b7fccdd47ed935c0
1827-
RNScreens: 5288a8dbeedb3c5051aa2d5658c1c553c050b80a
1826+
RNGestureHandler: 366823a3ebcc5ddd25550dbfe80e89779c4760b2
1827+
RNScreens: d86f05e9c243a063ca67cda7f4e05d28fe5c31d4
18281828
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
18291829
Yoga: 4ef80d96a5534f0e01b3055f17d1e19a9fc61b63
18301830

example/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"scripts": {
66
"android": "react-native run-android",
77
"build:android": "npm run mkdist && react-native bundle --entry-file index.js --platform android --dev true --bundle-output dist/main.android.jsbundle --assets-dest dist && react-native build-android --extra-params \"--no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a\"",
8-
"build:ios": "npm run mkdist && react-native bundle --entry-file index.js --platform ios --dev true --bundle-output dist/main.ios.jsbundle --assets-dest dist && react-native build-ios --scheme SwiftuiTabviewExample --mode Debug --extra-params \"-sdk iphonesimulator CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ GCC_OPTIMIZATION_LEVEL=0 GCC_PRECOMPILE_PREFIX_HEADER=YES ASSETCATALOG_COMPILER_OPTIMIZATION=time DEBUG_INFORMATION_FORMAT=dwarf COMPILER_INDEX_STORE_ENABLE=NO\"",
8+
"build:ios": "npm run mkdist && react-native bundle --entry-file index.js --platform ios --dev true --bundle-output dist/main.ios.jsbundle --assets-dest dist && react-native build-ios --scheme ReactNativeBottomTabs --mode Debug --extra-params \"-sdk iphonesimulator CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ GCC_OPTIMIZATION_LEVEL=0 GCC_PRECOMPILE_PREFIX_HEADER=YES ASSETCATALOG_COMPILER_OPTIMIZATION=time DEBUG_INFORMATION_FORMAT=dwarf COMPILER_INDEX_STORE_ENABLE=NO\"",
99
"build:visionos": "npm run mkdist && react-native bundle --entry-file index.js --platform ios --dev true --bundle-output dist/main.visionos.jsbundle --assets-dest dist",
1010
"ios": "react-native run-ios",
1111
"mkdist": "node -e \"require('node:fs').mkdirSync('dist', { recursive: true, mode: 0o755 })\"",
@@ -22,6 +22,7 @@
2222
"react": "18.3.1",
2323
"react-native": "0.75.3",
2424
"react-native-gesture-handler": "^2.20.0",
25+
"react-native-paper": "^5.12.5",
2526
"react-native-safe-area-context": "^4.11.0",
2627
"react-native-screens": "^3.34.0"
2728
},

example/src/App.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,20 @@ import { SafeAreaProvider } from 'react-native-safe-area-context';
1818
import JSBottomTabs from './Examples/JSBottomTabs';
1919
import ThreeTabs from './Examples/ThreeTabs';
2020
import FourTabs from './Examples/FourTabs';
21+
import MaterialBottomTabs from './Examples/MaterialBottomTabs';
22+
import SFSymbols from './Examples/SFSymbols';
2123

2224
const examples = [
2325
{ component: ThreeTabs, name: 'Three Tabs' },
2426
{ component: FourTabs, name: 'Four Tabs' },
27+
{ component: SFSymbols, name: 'SF Symbols' },
2528
{
2629
component: FourTabs,
2730
name: 'Four Tabs - No header',
2831
screenOptions: { headerShown: false },
2932
},
3033
{ component: JSBottomTabs, name: 'JS Bottom Tabs' },
34+
{ component: MaterialBottomTabs, name: 'Material (JS) Bottom Tabs' },
3135
];
3236

3337
function App() {

0 commit comments

Comments
 (0)