Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Handle custom icons #8

Merged
merged 9 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,8 @@ jobs:
- name: Install cocoapods
if: env.turbo_cache_hit != 1 && steps.cocoapods-cache.outputs.cache-hit != 'true'
run: |
cd example/ios
pod install
cd example
pod install --project-directory=ios
env:
NO_FLIPPER: 1

Expand Down
68 changes: 57 additions & 11 deletions android/src/main/java/com/rcttabview/RCTTabView.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
package com.rcttabview

import android.content.Context
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.net.Uri
import android.view.Choreographer
import android.view.MenuItem
import androidx.appcompat.content.res.AppCompatResources
import com.facebook.common.references.CloseableReference
import com.facebook.datasource.DataSources
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imagepipeline.image.CloseableBitmap
import com.facebook.imagepipeline.request.ImageRequestBuilder
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.WritableMap
import com.facebook.react.views.imagehelper.ImageSource
import com.facebook.react.views.imagehelper.ImageSource.Companion.getTransparentBitmapImageSource
import com.google.android.material.bottomnavigation.BottomNavigationView


class ReactBottomNavigationView(context: Context) : BottomNavigationView(context) {
private val ANIMATION_DURATION: Long = 300
private val icons: MutableMap<Int, ImageSource> = mutableMapOf()

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

fun updateItems(items: MutableList<TabInfo>) {
this.items = items
// TODO: This doesn't work with hot reload. It clears all menu items
menu.clear()
items.forEachIndexed {index, item ->
val menuItem = menu.add(0, index, 0, item.title)
val iconResourceId = resources.getIdentifier(
item.icon, "drawable", context.packageName
)
if (iconResourceId != 0) {
menuItem.icon = AppCompatResources.getDrawable(context, iconResourceId)
} else {
menuItem.setIcon(android.R.drawable.btn_star) // fallback icon
val menuItem = getOrCreateItem(index, item.title)
if (icons.containsKey(index)) {
menuItem.icon = getDrawable(icons[index]!!)
}
if (item.badge.isNotEmpty()) {
val badge = this.getOrCreateBadge(index)
Expand All @@ -84,6 +89,47 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
}
}

private fun getOrCreateItem(index: Int, title: String): MenuItem {
return menu.findItem(index) ?: menu.add(0, index, 0, title)
}

fun setIcons(icons: ReadableArray?) {
if (icons == null || icons.size() == 0) {
return
}

for (idx in 0 until icons.size()) {
val source = icons.getMap(idx)
var imageSource =
ImageSource(
context,
source.getString("uri")
)
if (Uri.EMPTY == imageSource.uri) {
imageSource = getTransparentBitmapImageSource(context)
}
this.icons[idx] = imageSource

// Update existing item if exists.
menu.findItem(idx)?.let { menuItem ->
menuItem.icon = getDrawable(imageSource)
}
}
}

private fun getDrawable(imageSource: ImageSource): Drawable {
// TODO: Check if this can be done using some built-in React Native class
val imageRequest = ImageRequestBuilder.newBuilderWithSource(imageSource.uri).build()
val dataSource = Fresco.getImagePipeline().fetchDecodedImage(imageRequest, context)
val result = DataSources.waitForFinalResult(dataSource) as CloseableReference<CloseableBitmap>
val bitmap = result.get().underlyingBitmap

CloseableReference.closeSafely(result)
dataSource.close()

return BitmapDrawable(resources, bitmap)
}

// Fixes issues with BottomNavigationView children layouting.
private fun measureAndLayout() {
measure(
Expand Down
7 changes: 5 additions & 2 deletions android/src/main/java/com/rcttabview/RCTTabViewViewManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import com.facebook.yoga.YogaNode

data class TabInfo(
val key: String,
val icon: String,
val title: String,
val badge: String
)
Expand All @@ -40,7 +39,6 @@ class RCTTabViewViewManager :
itemsArray.add(
TabInfo(
key = item.getString("key") ?: "",
icon = item.getString("icon") ?: "",
title = item.getString("title") ?: "",
badge = item.getString("badge") ?: ""
)
Expand All @@ -57,6 +55,11 @@ class RCTTabViewViewManager :
}
}

@ReactProp(name = "icons")
fun setIcons(view: ReactBottomNavigationView, icons: ReadableArray?) {
view.setIcons(icons)
}

public override fun createViewInstance(context: ThemedReactContext): ReactBottomNavigationView {
eventDispatcher = context.getNativeModule(UIManagerModule::class.java)!!.eventDispatcher
val view = ReactBottomNavigationView(context)
Expand Down
Binary file added example/assets/icons/article_dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added example/assets/icons/chat_dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added example/assets/icons/grid_dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added example/assets/icons/person_dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 9 additions & 9 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1774,15 +1774,15 @@ SPEC CHECKSUMS:
React-CoreModules: 2d68c251bc4080028f2835fa47504e8f20669a21
React-cxxreact: bb0dc212b515d6dba6c6ddc4034584e148857db9
React-debug: fd0ed8ecd5f8a23c7daf5ceaca8aa722a4d083fd
React-defaultsnativemodule: 371dc516e5020f8b87f1d32f8fa6872cafcc2081
React-domnativemodule: 5d1288b9b8666b818a1004b56a03befc00eb5698
React-defaultsnativemodule: 0d824306a15dd80e2bea12f4079fbeff9712b301
React-domnativemodule: 195491d7c1725befd636f84c67bf229203fc7d07
React-Fabric: c12ce848f72cba42fb9e97a73a7c99abc6353f23
React-FabricComponents: 7813d5575c8ea2cda0fef9be4ff9d10987cba512
React-FabricImage: c511a5d612479cb4606edf3557c071956c8735f6
React-featureflags: cf78861db9318ae29982fa8953c92d31b276c9ac
React-featureflagsnativemodule: e774cf495486b0e2a8b324568051d6b4c722fa93
React-featureflagsnativemodule: 54f6decea27c187c2127e3669a7f5bf2e145e637
React-graphics: 7572851bca7242416b648c45d6af87d93d29281e
React-idlecallbacksnativemodule: d2009bad67ef232a0ee586f53193f37823e81ef1
React-idlecallbacksnativemodule: 7d21b0e071c3e02bcc897d2c3db51319642dd466
React-ImageManager: aedf54d34d4475c66f4c3da6b8359b95bee904e4
React-jsc: 92ac98e0e03ee54fdaa4ac3936285a4fdb166fab
React-jserrorhandler: 0c8949672a00f2a502c767350e591e3ec3d82fb3
Expand All @@ -1792,8 +1792,8 @@ SPEC CHECKSUMS:
React-jsitracing: 3935b092f85bb1e53b8cf8a00f572413648af46b
React-logger: 4072f39df335ca443932e0ccece41fbeb5ca8404
React-Mapbuffer: 714f2fae68edcabfc332b754e9fbaa8cfc68fdd4
React-microtasksnativemodule: 987cf7e0e0e7129250a48b807e70d3b906c726cf
react-native-bottom-tabs: 894d1fb8fc4e6d525b2da35e83e00e18c420cdf2
React-microtasksnativemodule: 618b64238e43ef3154079f193aa6649e5320ae19
react-native-bottom-tabs: 5662b5e3b5968bec6258b9d6f1a0a834bd3f7553
react-native-safe-area-context: 851c62c48dce80ccaa5637b6aa5991a1bc36eca9
React-nativeconfig: 4a9543185905fe41014c06776bf126083795aed9
React-NativeModulesApple: 651670a799672bd54469f2981d91493dda361ddf
Expand All @@ -1820,11 +1820,11 @@ SPEC CHECKSUMS:
React-utils: b2baee839fb869f732d617b97dcfa384b4b4fdb3
ReactCodegen: f177b8fd67788c5c6ff45a39c7482c5f8d77ace6
ReactCommon: 627bd3192ef01a351e804e9709673d3741d38fec
ReactNativeHost: 99c0ffb175cd69de2ac9a70892cd22dac65ea79d
ReactNativeHost: 62249d6e1e42a969159946c035c1cd3f4b1035dd
ReactTestApp-DevSupport: b7cd76a3aeee6167f5e14d82f09685059152c426
ReactTestApp-Resources: 7db90c026cccdf40cfa495705ad436ccc4d64154
RNGestureHandler: 18b9b5d65c77c4744a640f69b7fccdd47ed935c0
RNScreens: 5288a8dbeedb3c5051aa2d5658c1c553c050b80a
RNGestureHandler: 366823a3ebcc5ddd25550dbfe80e89779c4760b2
RNScreens: d86f05e9c243a063ca67cda7f4e05d28fe5c31d4
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
Yoga: 4ef80d96a5534f0e01b3055f17d1e19a9fc61b63

Expand Down
3 changes: 2 additions & 1 deletion example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"scripts": {
"android": "react-native run-android",
"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\"",
"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\"",
"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\"",
"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",
"ios": "react-native run-ios",
"mkdist": "node -e \"require('node:fs').mkdirSync('dist', { recursive: true, mode: 0o755 })\"",
Expand All @@ -22,6 +22,7 @@
"react": "18.3.1",
"react-native": "0.75.3",
"react-native-gesture-handler": "^2.20.0",
"react-native-paper": "^5.12.5",
"react-native-safe-area-context": "^4.11.0",
"react-native-screens": "^3.34.0"
},
Expand Down
4 changes: 4 additions & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,20 @@
import JSBottomTabs from './Examples/JSBottomTabs';
import ThreeTabs from './Examples/ThreeTabs';
import FourTabs from './Examples/FourTabs';
import MaterialBottomTabs from './Examples/MaterialBottomTabs';
import SFSymbols from './Examples/SFSymbols';

const examples = [
{ component: ThreeTabs, name: 'Three Tabs' },
{ component: FourTabs, name: 'Four Tabs' },
{ component: SFSymbols, name: 'SF Symbols' },
{
component: FourTabs,
name: 'Four Tabs - No header',
screenOptions: { headerShown: false },
},
{ component: JSBottomTabs, name: 'JS Bottom Tabs' },
{ component: MaterialBottomTabs, name: 'Material (JS) Bottom Tabs' },
];

function App() {
Expand Down Expand Up @@ -66,7 +70,7 @@
name="BottomTabs Example"
component={App}
options={{
headerRight: () => (

Check warning on line 73 in example/src/App.tsx

View workflow job for this annotation

GitHub Actions / lint

Do not define components during render. React will see a new component type on every render and destroy the entire subtree’s DOM nodes and state (https://reactjs.org/docs/reconciliation.html#elements-of-different-types). Instead, move this component definition out of the parent component “Navigation” and pass data as props. If you want to allow component creation in props, set allowAsProps option to true
<Button
onPress={() =>
Alert.alert(
Expand Down
22 changes: 18 additions & 4 deletions example/src/Examples/FourTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,29 @@ import { Chat } from '../Screens/Chat';
export default function FourTabs() {
const [index, setIndex] = useState(0);
const [routes] = useState([
{ key: 'article', title: 'Article', icon: 'document.fill', badge: '!' },
{
key: 'article',
title: 'Article',
focusedIcon: require('../../assets/icons/article_dark.png'),
unfocusedIcon: require('../../assets/icons/chat_dark.png'),
badge: '!',
},
{
key: 'albums',
title: 'Albums',
icon: 'square.grid.2x2.fill',
focusedIcon: require('../../assets/icons/grid_dark.png'),
badge: '5',
},
{ key: 'contacts', title: 'Contacts', icon: 'person.fill' },
{ key: 'chat', title: 'Chat', icon: 'keyboard' },
{
key: 'contacts',
focusedIcon: require('../../assets/icons/person_dark.png'),
title: 'Contacts',
},
{
key: 'chat',
focusedIcon: require('../../assets/icons/chat_dark.png'),
title: 'Chat',
},
]);

const renderScene = SceneMap({
Expand Down
61 changes: 61 additions & 0 deletions example/src/Examples/MaterialBottomTabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { createMaterialBottomTabNavigator } from 'react-native-paper/react-navigation';
import { Article } from '../Screens/Article';
import { Albums } from '../Screens/Albums';
import { Contacts } from '../Screens/Contacts';
import { Chat } from '../Screens/Chat';
import { Image, type ImageSourcePropType } from 'react-native';

const Tab = createMaterialBottomTabNavigator();

const TabBarIcon = ({ source }: { source: ImageSourcePropType }) => (
<Image style={{ width: 20, height: 23 }} source={source} />

Check warning on line 11 in example/src/Examples/MaterialBottomTabs.tsx

View workflow job for this annotation

GitHub Actions / lint

Inline style: { width: 20, height: 23 }
);

function MaterialBottomTabs() {
return (
<Tab.Navigator shifting>
<Tab.Screen
name="Article"
component={Article}
options={{
tabBarIcon: () => (

Check warning on line 21 in example/src/Examples/MaterialBottomTabs.tsx

View workflow job for this annotation

GitHub Actions / lint

Do not define components during render. React will see a new component type on every render and destroy the entire subtree’s DOM nodes and state (https://reactjs.org/docs/reconciliation.html#elements-of-different-types). Instead, move this component definition out of the parent component “MaterialBottomTabs” and pass data as props. If you want to allow component creation in props, set allowAsProps option to true
<TabBarIcon
source={require('../../assets/icons/article_dark.png')}
/>
),
}}
/>
<Tab.Screen
name="Albums"
component={Albums}
options={{
tabBarIcon: () => (

Check warning on line 32 in example/src/Examples/MaterialBottomTabs.tsx

View workflow job for this annotation

GitHub Actions / lint

Do not define components during render. React will see a new component type on every render and destroy the entire subtree’s DOM nodes and state (https://reactjs.org/docs/reconciliation.html#elements-of-different-types). Instead, move this component definition out of the parent component “MaterialBottomTabs” and pass data as props. If you want to allow component creation in props, set allowAsProps option to true
<TabBarIcon source={require('../../assets/icons/grid_dark.png')} />
),
}}
/>
<Tab.Screen
name="Contacts"
component={Contacts}
options={{
tabBarIcon: () => (

Check warning on line 41 in example/src/Examples/MaterialBottomTabs.tsx

View workflow job for this annotation

GitHub Actions / lint

Do not define components during render. React will see a new component type on every render and destroy the entire subtree’s DOM nodes and state (https://reactjs.org/docs/reconciliation.html#elements-of-different-types). Instead, move this component definition out of the parent component “MaterialBottomTabs” and pass data as props. If you want to allow component creation in props, set allowAsProps option to true
<TabBarIcon
source={require('../../assets/icons/person_dark.png')}
/>
),
}}
/>
<Tab.Screen
name="Chat"
component={Chat}
options={{
tabBarIcon: () => (

Check warning on line 52 in example/src/Examples/MaterialBottomTabs.tsx

View workflow job for this annotation

GitHub Actions / lint

Do not define components during render. React will see a new component type on every render and destroy the entire subtree’s DOM nodes and state (https://reactjs.org/docs/reconciliation.html#elements-of-different-types). Instead, move this component definition out of the parent component “MaterialBottomTabs” and pass data as props. If you want to allow component creation in props, set allowAsProps option to true
<TabBarIcon source={require('../../assets/icons/chat_dark.png')} />
),
}}
/>
</Tab.Navigator>
);
}

export default MaterialBottomTabs;
54 changes: 54 additions & 0 deletions example/src/Examples/SFSymbols.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import TabView, { SceneMap } from 'react-native-bottom-tabs';
import { useState } from 'react';
import { Article } from '../Screens/Article';
import { Albums } from '../Screens/Albums';
import { Contacts } from '../Screens/Contacts';
import { Platform } from 'react-native';

const isAndroid = Platform.OS === 'android';

export default function SFSymbols() {
const [index, setIndex] = useState(0);
const [routes] = useState([
{
key: 'article',
title: 'Article',
focusedIcon: isAndroid
? require('../../assets/icons/article_dark.png')
: { sfSymbol: 'document.fill' },
unfocusedIcon: isAndroid
? require('../../assets/icons/chat_dark.png')
: { sfSymbol: 'bubble.left.fill' },
badge: '!',
},
{
key: 'albums',
title: 'Albums',
focusedIcon: isAndroid
? require('../../assets/icons/grid_dark.png')
: { sfSymbol: 'square.grid.3x2.fill' },
badge: '5',
},
{
key: 'contacts',
focusedIcon: isAndroid
? require('../../assets/icons/person_dark.png')
: { sfSymbol: 'person.fill' },
title: 'Contacts',
},
]);

const renderScene = SceneMap({
article: Article,
albums: Albums,
contacts: Contacts,
});

return (
<TabView
navigationState={{ index, routes }}
onIndexChange={setIndex}
renderScene={renderScene}
/>
);
}
16 changes: 13 additions & 3 deletions example/src/Examples/ThreeTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,24 @@ import { Contacts } from '../Screens/Contacts';
export default function ThreeTabs() {
const [index, setIndex] = useState(0);
const [routes] = useState([
{ key: 'article', title: 'Article', icon: 'document.fill', badge: '!' },
{
key: 'article',
title: 'Article',
focusedIcon: require('../../assets/icons/article_dark.png'),
unfocusedIcon: require('../../assets/icons/chat_dark.png'),
badge: '!',
},
{
key: 'albums',
title: 'Albums',
icon: 'square.grid.2x2.fill',
focusedIcon: require('../../assets/icons/grid_dark.png'),
badge: '5',
},
{ key: 'contacts', title: 'Contacts', icon: 'person.fill' },
{
key: 'contacts',
focusedIcon: require('../../assets/icons/person_dark.png'),
title: 'Contacts',
},
]);

const renderScene = SceneMap({
Expand Down
Loading
Loading