diff --git a/.changeset/tall-icons-grow.md b/.changeset/tall-icons-grow.md new file mode 100644 index 00000000..3bb3ef65 --- /dev/null +++ b/.changeset/tall-icons-grow.md @@ -0,0 +1,6 @@ +--- +'react-native-bottom-tabs': minor +'@bottom-tabs/react-navigation': minor +--- + +Add a per-screen option for customizing tab image icon size. diff --git a/docs/docs/docs/guides/standalone-usage.mdx b/docs/docs/docs/guides/standalone-usage.mdx index 29c06bde..deec7fbd 100644 --- a/docs/docs/docs/guides/standalone-usage.mdx +++ b/docs/docs/docs/guides/standalone-usage.mdx @@ -236,6 +236,7 @@ Each route in the `routes` array can have the following properties: - `title`: Display title for the tab - `focusedIcon`: Icon to show when tab is active - `unfocusedIcon`: Icon to show when tab is inactive (optional) +- `iconSize`: Width and height of the image icon in logical pixels. Uses the native default when omitted. - `iconRenderingMode`: Rendering mode for icons. Use `'original'` to preserve multicolor icons instead of applying the native tab tint. - `badge`: Badge text to display on the tab - `activeTintColor`: Custom active tint color for this specific tab @@ -290,6 +291,13 @@ Function to get the rendering mode for an image icon. Use `original` to preserve the original colors of multicolor image icons, such as logos or branded assets, instead of applying the native tab tint. +#### `getIconSize` + +Function to get the width and height of an image icon in logical pixels. + +- Default: Uses `route.iconSize` +- When omitted: Uses the native platform default + #### `getHidden` Function to determine if a tab should be hidden. diff --git a/docs/docs/docs/guides/usage-with-react-navigation.mdx b/docs/docs/docs/guides/usage-with-react-navigation.mdx index 8ff246ac..374e493d 100644 --- a/docs/docs/docs/guides/usage-with-react-navigation.mdx +++ b/docs/docs/docs/guides/usage-with-react-navigation.mdx @@ -286,6 +286,21 @@ Function that given `{ focused: boolean }` returns `ImageSource` or `AppleIcon` SF Symbols are only supported on Apple platforms. ::: +#### `tabBarIconSize` + +Width and height of the tab image icon in logical pixels. This can be set per screen, and uses the native platform default when omitted. + +```tsx + ({ uri: avatarFileUri }), + tabBarIconSize: 34, + }} +/> +``` + #### `tabBarIconRenderingMode` Rendering mode for image icons. diff --git a/packages/example-shared/src/App.tsx b/packages/example-shared/src/App.tsx index 420d3427..1dc55226 100644 --- a/packages/example-shared/src/App.tsx +++ b/packages/example-shared/src/App.tsx @@ -45,6 +45,7 @@ import TabBarHidden from './Examples/TabBarHidden'; import CustomTabBar from './Examples/CustomTabBar'; import NativeBottomTabsTabBarHidden from './Examples/NativeBottomTabsTabBarHidden'; import OriginalIconColors from './Examples/OriginalIconColors'; +import IconSizes from './Examples/IconSizes'; import NativeBottomTabsOriginalIcons from './Examples/NativeBottomTabsOriginalIcons'; import { useLogger } from '@react-navigation/devtools'; import LazyTabs from './Examples/LazyTabs'; @@ -126,6 +127,10 @@ const examples = [ component: OriginalIconColors, name: 'Original icon colors', }, + { + component: IconSizes, + name: 'Icon sizes', + }, { component: FourTabsRippleColor, name: 'Four Tabs with ripple Color', diff --git a/packages/example-shared/src/Examples/IconSizes.tsx b/packages/example-shared/src/Examples/IconSizes.tsx new file mode 100644 index 00000000..ef78821a --- /dev/null +++ b/packages/example-shared/src/Examples/IconSizes.tsx @@ -0,0 +1,52 @@ +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 { Chat } from '../Screens/Chat'; + +const renderScene = SceneMap({ + article: Article, + albums: Albums, + contacts: Contacts, + chat: Chat, +}); + +export default function IconSizes() { + const [index, setIndex] = useState(0); + const [routes] = useState([ + { + key: 'article', + title: 'Default', + focusedIcon: require('../../assets/icons/article_dark.png'), + }, + { + key: 'albums', + title: 'Size 20', + focusedIcon: require('../../assets/icons/grid_dark.png'), + iconSize: 20, + }, + { + key: 'contacts', + title: 'Size 32', + focusedIcon: require('../../assets/icons/person_dark.png'), + iconSize: 32, + }, + { + key: 'chat', + title: 'Size 40', + focusedIcon: require('../../assets/icons/chat_dark.png'), + iconSize: 40, + }, + ]); + + return ( + + ); +} 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 66de5d2f..972b1b82 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 @@ -37,10 +37,14 @@ import com.facebook.react.common.assets.ReactFontManager import com.facebook.react.modules.core.ReactChoreographer import com.facebook.react.views.text.ReactTypefaceUtils import com.google.android.material.bottomnavigation.BottomNavigationView +import com.google.android.material.navigation.NavigationBarItemView import com.google.android.material.navigation.NavigationBarView.LABEL_VISIBILITY_AUTO import com.google.android.material.navigation.NavigationBarView.LABEL_VISIBILITY_LABELED import com.google.android.material.navigation.NavigationBarView.LABEL_VISIBILITY_UNLABELED import com.google.android.material.transition.platform.MaterialFadeThrough +import kotlin.math.roundToInt + +private data class DrawableCacheKey(val source: ImageSource, val size: Int) class ExtendedBottomNavigationView(context: Context) : BottomNavigationView(context) { override fun getMaxItemCount(): Int { @@ -59,7 +63,8 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) { var disablePageAnimations = false var items: MutableList = mutableListOf() private val iconSources: MutableMap = mutableMapOf() - private val drawableCache: MutableMap = mutableMapOf() + private var iconSizes: List = emptyList() + private val drawableCache: MutableMap = mutableMapOf() private var isLayoutEnqueued = false private var selectedItem: String? = null @@ -240,7 +245,10 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) { } } - fun updateItems(items: MutableList) { + fun updateItems(newItems: MutableList) { + val items = newItems.mapIndexed { index, item -> + item.copy(iconSize = iconSizes.getOrNull(index)?.takeIf { it > 0 }) + }.toMutableList() // If an item got removed, let's re-add all items if (items.size < this.items.size) { bottomNavigation.menu.clear() @@ -255,9 +263,9 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) { menuItem.isVisible = !item.hidden updateIconTintMode(menuItem, item) if (iconSources.containsKey(index)) { - getDrawable(iconSources[index]!!) { - menuItem.icon = it - updateIconTintMode(menuItem, item) + val iconSizePx = getIconSize(item) + getDrawable(iconSources[index]!!, iconSizePx) { + applyIcon(menuItem, index, iconSizePx, it) } } @@ -288,6 +296,8 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) { view.findViewById(com.google.android.material.R.id.navigation_bar_item_content_container) ?.setTabTestID(item.testID) + + updateIconSize(menuItem, item) } } } @@ -298,6 +308,11 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) { } } + fun setIconSizes(iconSizes: List) { + this.iconSizes = iconSizes + updateItems(items) + } + private fun getOrCreateItem(index: Int, title: String): MenuItem { return bottomNavigation.menu.findItem(index) ?: bottomNavigation.menu.add(0, index, 0, title) } @@ -344,11 +359,9 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) { // Update existing item if exists. bottomNavigation.menu.findItem(idx)?.let { menuItem -> - getDrawable(imageSource) { - menuItem.icon = it - items.getOrNull(idx)?.let { item -> - updateIconTintMode(menuItem, item) - } + val iconSizePx = getIconSize(items.getOrNull(idx)) + getDrawable(imageSource, iconSizePx) { + applyIcon(menuItem, idx, iconSizePx, it) } } } @@ -374,12 +387,16 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) { } @SuppressLint("CheckResult") - private fun getDrawable(imageSource: ImageSource, onDrawableReady: (Drawable?) -> Unit) { - drawableCache[imageSource]?.let { + private fun getDrawable( + imageSource: ImageSource, + iconSizePx: Int, + onDrawableReady: (Drawable?) -> Unit + ) { + val cacheKey = DrawableCacheKey(imageSource, iconSizePx) + drawableCache[cacheKey]?.let { onDrawableReady(it) return } - val iconSizePx = bottomNavigation.itemIconSize val request = ImageRequest.Builder(context) .data(imageSource.getUri(context)) .size(CoilSize(iconSizePx, iconSizePx)) @@ -388,7 +405,7 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) { .target { drawable -> post { val stateDrawable = drawable.asDrawable(context.resources) - drawableCache[imageSource] = stateDrawable + drawableCache[cacheKey] = stateDrawable onDrawableReady(stateDrawable) } } @@ -402,6 +419,34 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) { imageLoader.enqueue(request) } + private fun getIconSize(item: TabInfo?): Int { + val iconSize = item?.iconSize ?: return bottomNavigation.itemIconSize + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + iconSize.toFloat(), + resources.displayMetrics + ).roundToInt() + } + + private fun applyIcon(menuItem: MenuItem, index: Int, iconSizePx: Int, drawable: Drawable?) { + // Icons load asynchronously, so read the latest item instead of the one captured at request time. + val item = items.getOrNull(index) + // The icon size changed while loading, a request for the new size is already in flight. + if (getIconSize(item) != iconSizePx) return + + menuItem.icon = drawable + item?.let { + updateIconTintMode(menuItem, it) + updateIconSize(menuItem, it) + } + } + + private fun updateIconSize(menuItem: MenuItem, item: TabInfo) { + val itemView = bottomNavigation.findViewById(menuItem.itemId) + ?: return + itemView.setIconSize(getIconSize(item)) + } + fun setBarTintColor(color: Int?) { // Set the color, either using the active background color or a default color. val backgroundColor = diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewManager.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewManager.kt index 3d0c872b..c0ebc44c 100644 --- a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewManager.kt +++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewManager.kt @@ -24,6 +24,7 @@ data class TabInfo( val badgeBackgroundColor: Int?, val badgeTextColor: Int?, val activeTintColor: Int?, + val iconSize: Double?, val iconRenderingMode: String?, val hidden: Boolean, val testID: String? @@ -105,6 +106,7 @@ class RCTTabViewManager(context: ReactApplicationContext) : badgeBackgroundColor = if (item.hasKey("badgeBackgroundColor")) item.getInt("badgeBackgroundColor") else null, badgeTextColor = if (item.hasKey("badgeTextColor")) item.getInt("badgeTextColor") else null, activeTintColor = if (item.hasKey("activeTintColor")) item.getInt("activeTintColor") else null, + iconSize = null, iconRenderingMode = if (item.hasKey("iconRenderingMode")) item.getString("iconRenderingMode") else null, hidden = if (item.hasKey("hidden")) item.getBoolean("hidden") else false, testID = item.getString("testID") @@ -126,6 +128,16 @@ class RCTTabViewManager(context: ReactApplicationContext) : view.setIcons(value) } + override fun setIconSizes(view: ReactBottomNavigationView?, value: ReadableArray?) { + view?.setIconSizes( + if (value == null) { + emptyList() + } else { + (0 until value.size()).map { value.getDouble(it) } + } + ) + } + override fun setFocusedIcons(view: ReactBottomNavigationView?, value: ReadableArray?) { } diff --git a/packages/react-native-bottom-tabs/ios/Bridge/BottomTabsBridge.h b/packages/react-native-bottom-tabs/ios/Bridge/BottomTabsBridge.h index 798e2cf0..011b57b9 100644 --- a/packages/react-native-bottom-tabs/ios/Bridge/BottomTabsBridge.h +++ b/packages/react-native-bottom-tabs/ios/Bridge/BottomTabsBridge.h @@ -38,6 +38,7 @@ @property(nonatomic, strong) NSNumber *fontSize; @property(nonatomic) BOOL tabBarHidden; @property(nonatomic, copy) NSArray *itemsData; +@property(nonatomic, copy) NSArray *iconSizes; - (void)setImageLoader:(RCTImageLoader *)imageLoader; - (void)insertChild:(UIView *)child atIndex:(NSInteger)index; - (void)removeChildAtIndex:(NSInteger)index; @@ -53,6 +54,7 @@ FOUNDATION_EXPORT NSObject *RNCCreateBottomAccessoryProvider(id(_props); const auto &newViewProps = *std::static_pointer_cast(props); + if (oldViewProps.iconSizes != newViewProps.iconSizes) { + NSMutableArray *iconSizes = [NSMutableArray arrayWithCapacity:newViewProps.iconSizes.size()]; + for (const auto iconSize : newViewProps.iconSizes) { + [iconSizes addObject:@(iconSize)]; + } + _tabViewProvider.iconSizes = iconSizes; + } + if (oldViewProps.items != newViewProps.items) { _tabViewProvider.itemsData = convertItemsToArray(newViewProps.items); } @@ -237,6 +245,7 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & sfSymbol:RCTNSStringFromStringNilIfEmpty(item.sfSymbol) focusedSfSymbol:RCTNSStringFromStringNilIfEmpty(item.focusedSfSymbol) activeTintColor:RCTUIColorFromSharedColor(item.activeTintColor) + iconSize:nil iconRenderingMode:RCTNSStringFromStringNilIfEmpty(item.iconRenderingMode) hidden:item.hidden testID:RCTNSStringFromStringNilIfEmpty(item.testID) diff --git a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift index ca8ff60a..2e2f4672 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift @@ -312,6 +312,7 @@ struct TabViewImpl: View { icon: assetIcon.map(IconSource.image) ?? .sfSymbol(tabData.sfSymbol), focusedIcon: focusedAssetIcon.map(IconSource.image) ?? tabData.focusedSfSymbol.map(IconSource.sfSymbol), title: tabData.title, + iconSize: iconSize(for: tabData), inactiveTintColor: props.inactiveTintColor, activeTintColor: tabData.activeTintColor ?? props.activeTintColor, preservesOriginalIconColors: tabData.iconRenderingMode == "original", @@ -351,6 +352,7 @@ struct TabViewImpl: View { icon: icon, title: inputs.title, color: inputs.inactiveTintColor, + iconSize: inputs.iconSize, preservesOriginalIconColors: inputs.preservesOriginalIconColors, props: props ), @@ -358,6 +360,7 @@ struct TabViewImpl: View { icon: selectedIcon, title: inputs.title, color: inputs.activeTintColor, + iconSize: inputs.iconSize, preservesOriginalIconColors: inputs.preservesOriginalIconColors, props: props ), @@ -405,6 +408,7 @@ struct TabViewImpl: View { let icon: IconSource let focusedIcon: IconSource? let title: String + let iconSize: CGSize let inactiveTintColor: UIColor? let activeTintColor: UIColor? let preservesOriginalIconColors: Bool @@ -466,11 +470,11 @@ struct TabViewImpl: View { icon: UIImage, title: String, color: UIColor?, + iconSize: CGSize, preservesOriginalIconColors: Bool = false, props: TabViewProps ) -> UIImage { let color = color ?? .label - let iconSize = CGSize(width: 27, height: 27) let font = TabBarFontSize.createFontAttributes( size: props.fontSize.map(CGFloat.init) ?? TabBarFontSize.defaultSize, @@ -525,6 +529,14 @@ struct TabViewImpl: View { return image.withRenderingMode(.alwaysOriginal) } + private func iconSize(for tabData: TabInfo) -> CGSize { + guard let value = tabData.iconSize?.doubleValue, value > 0 else { + return defaultIconSize + } + + return CGSize(width: value, height: value) + } + private func aspectFitRect(size: CGSize, in rect: CGRect) -> CGRect { guard size.width > 0, size.height > 0 else { return rect diff --git a/packages/react-native-bottom-tabs/ios/TabViewProvider.swift b/packages/react-native-bottom-tabs/ios/TabViewProvider.swift index e5663453..68292fba 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewProvider.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewProvider.swift @@ -10,6 +10,7 @@ public final class TabInfo: NSObject { public let sfSymbol: String public let focusedSfSymbol: String? public let activeTintColor: PlatformColor? + public var iconSize: NSNumber? public let iconRenderingMode: String? public let hidden: Bool public let testID: String? @@ -23,6 +24,7 @@ public final class TabInfo: NSObject { sfSymbol: String, focusedSfSymbol: String?, activeTintColor: PlatformColor?, + iconSize: NSNumber?, iconRenderingMode: String?, hidden: Bool, testID: String?, @@ -35,6 +37,7 @@ public final class TabInfo: NSObject { self.sfSymbol = sfSymbol self.focusedSfSymbol = focusedSfSymbol self.activeTintColor = activeTintColor + self.iconSize = iconSize self.iconRenderingMode = iconRenderingMode self.hidden = hidden self.testID = testID @@ -51,13 +54,14 @@ public final class TabInfo: NSObject { func onLayout(size: CGSize, reactTag: NSNumber?) } +let defaultIconSize = CGSize(width: 27, height: 27) + @objc public class TabViewProvider: PlatformView { private var imageLoader: RCTImageLoaderProtocol? private weak var delegate: TabViewProviderDelegate? private var props = TabViewProps() private var hostingController: PlatformHostingController? private var coalescingKey: UInt16 = 0 - private var iconSize = CGSize(width: 27, height: 27) @objc var onPageSelected: RCTDirectEventBlock? @@ -180,7 +184,13 @@ public final class TabInfo: NSObject { @objc public var itemsData: [TabInfo] = [] { didSet { - props.items = itemsData + updateItems() + } + } + + @objc public var iconSizes: [NSNumber] = [] { + didSet { + updateItems() } } @@ -280,13 +290,20 @@ public final class TabInfo: NSObject { guard let image else { return } DispatchQueue.main.async { [weak self] in guard let self else { return } - let icon = image.resizeImageTo(size: iconSize) + guard let resizedIcon = image.resizeImageTo(size: iconSize(at: index)) else { + return + } + #if os(iOS) + let icon = alignedIcon(resizedIcon, at: index) + #else + let icon = resizedIcon + #endif #if os(iOS) if props.experimentalBakedTintColors { if focused { - props.focusedIcons[index] = icon?.withRenderingMode(.alwaysTemplate) + props.focusedIcons[index] = icon.withRenderingMode(.alwaysTemplate) } else { - props.icons[index] = icon?.withRenderingMode(.alwaysTemplate) + props.icons[index] = icon.withRenderingMode(.alwaysTemplate) } } else { if focused { @@ -309,4 +326,44 @@ public final class TabInfo: NSObject { } } } + + private func iconSize(at index: Int) -> CGSize { + guard let value = props.items[safe: index]?.iconSize?.doubleValue, value > 0 else { + return defaultIconSize + } + + return CGSize(width: value, height: value) + } + + private func updateItems() { + let previousIconSizes = props.items.map(\.iconSize) + for (index, item) in itemsData.enumerated() { + if let value = iconSizes[safe: index], value.doubleValue > 0 { + item.iconSize = value + } else { + item.iconSize = nil + } + } + props.items = itemsData + if previousIconSizes != itemsData.map(\.iconSize) { + loadIcons(icons, focused: false) + loadIcons(focusedIcons, focused: true) + } + } + + #if os(iOS) + private func alignedIcon(_ image: UIImage, at index: Int) -> UIImage { + let size = iconSize(at: index) + let horizontalInset = (size.width - defaultIconSize.width) / 2 + let verticalInset = (size.height - defaultIconSize.height) / 2 + return image.withAlignmentRectInsets( + UIEdgeInsets( + top: verticalInset, + left: horizontalInset, + bottom: verticalInset, + right: horizontalInset + ) + ) + } + #endif } diff --git a/packages/react-native-bottom-tabs/src/TabView.tsx b/packages/react-native-bottom-tabs/src/TabView.tsx index 0acfb842..dafec055 100644 --- a/packages/react-native-bottom-tabs/src/TabView.tsx +++ b/packages/react-native-bottom-tabs/src/TabView.tsx @@ -146,6 +146,11 @@ interface Props { focused: boolean; }) => ImageSource | AppleIcon | undefined | null; + /** + * Get the size of the tab image icon, uses `route.iconSize` by default. + */ + getIconSize?: (props: { route: Route }) => number | undefined; + /** * Get the rendering mode for the tab icon, uses `route.iconRenderingMode` by default. * @@ -262,6 +267,7 @@ const TabView = ({ getActiveTintColor = ({ route }: { route: Route }) => route.activeTintColor, getTestID = ({ route }: { route: Route }) => route.testID, getRole = ({ route }: { route: Route }) => route.role, + getIconSize = ({ route }: { route: Route }) => route.iconSize, getIconRenderingMode = ({ route }: { route: Route }) => route.iconRenderingMode, getSceneStyle = ({ route }: { route: Route }) => route.style, @@ -383,6 +389,11 @@ const TabView = ({ ] ); + const iconSizes = React.useMemo( + () => trimmedRoutes.map((route) => getIconSize({ route }) ?? 0), + [getIconSize, trimmedRoutes] + ); + const resolvedIconAssets: ImageSource[] = React.useMemo( () => // Pass empty object for icons that are not provided to avoid index mismatch on native side. @@ -458,6 +469,7 @@ const TabView = ({ {...tabLabelStyle} style={styles.fullWidth} items={items} + iconSizes={iconSizes} // When rendering a custom tab bar, icons can be React elements, which will not be properly resolved. icons={renderCustomTabBar ? undefined : resolvedIconAssets} focusedIcons={ diff --git a/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts b/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts index 50ecf7c4..bdd1ccb9 100644 --- a/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts +++ b/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts @@ -40,6 +40,7 @@ export type TabViewItems = ReadonlyArray<{ export interface TabViewProps extends ViewProps { items: TabViewItems; + iconSizes?: ReadonlyArray; selectedPage: string; onPageSelected?: DirectEventHandler; onTabLongPress?: DirectEventHandler; diff --git a/packages/react-native-bottom-tabs/src/types.ts b/packages/react-native-bottom-tabs/src/types.ts index 84223786..4cf27cc4 100644 --- a/packages/react-native-bottom-tabs/src/types.ts +++ b/packages/react-native-bottom-tabs/src/types.ts @@ -20,6 +20,7 @@ export type BaseRoute = { lazy?: boolean; focusedIcon?: ImageSourcePropType | AppleIcon; unfocusedIcon?: ImageSourcePropType | AppleIcon; + iconSize?: number; iconRenderingMode?: IconRenderingMode; activeTintColor?: string; hidden?: boolean; diff --git a/packages/react-navigation/src/types.ts b/packages/react-navigation/src/types.ts index eb1967cd..1f8b208e 100644 --- a/packages/react-navigation/src/types.ts +++ b/packages/react-navigation/src/types.ts @@ -71,6 +71,11 @@ export type NativeBottomTabNavigationOptions = { */ tabBarIcon?: (props: { focused: boolean }) => ImageSourcePropType | AppleIcon; + /** + * Width and height of the tab image icon in logical pixels. + */ + tabBarIconSize?: number; + /** * Rendering mode for the tab icon. Use `original` to preserve multicolor image icons. */ @@ -159,6 +164,7 @@ export type NativeBottomTabNavigationConfig = Partial< | 'renderScene' | 'getLazy' | 'getIcon' + | 'getIconSize' | 'getIconRenderingMode' | 'getLabelText' | 'getBadge' diff --git a/packages/react-navigation/src/views/NativeBottomTabView.tsx b/packages/react-navigation/src/views/NativeBottomTabView.tsx index 629bec4b..87c17b52 100644 --- a/packages/react-navigation/src/views/NativeBottomTabView.tsx +++ b/packages/react-navigation/src/views/NativeBottomTabView.tsx @@ -30,6 +30,9 @@ export default function NativeBottomTabView({ {...rest} navigationState={state} renderScene={({ route }) => descriptors[route.key]?.render()} + getIconSize={({ route }) => + descriptors[route.key]?.options.tabBarIconSize + } getActiveTintColor={({ route }) => { return descriptors[route.key]?.options.tabBarActiveTintColor; }}