diff --git a/android/src/main/java/com/rcttabview/RCTTabView.kt b/android/src/main/java/com/rcttabview/RCTTabView.kt index 79897c96..753f8ff2 100644 --- a/android/src/main/java/com/rcttabview/RCTTabView.kt +++ b/android/src/main/java/com/rcttabview/RCTTabView.kt @@ -2,6 +2,7 @@ package com.rcttabview import android.content.Context import android.content.res.ColorStateList +import android.graphics.Typeface import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable @@ -11,6 +12,8 @@ import android.view.Choreographer import android.view.HapticFeedbackConstants import android.view.MenuItem import android.view.View +import android.view.ViewGroup +import android.widget.TextView import androidx.appcompat.content.res.AppCompatResources import com.facebook.common.references.CloseableReference import com.facebook.datasource.DataSources @@ -20,8 +23,10 @@ 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.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 @@ -37,6 +42,9 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context private val checkedStateSet = intArrayOf(android.R.attr.state_checked) private val uncheckedStateSet = intArrayOf(-android.R.attr.state_checked) private var hapticFeedbackEnabled = true + private var fontSize: Int? = null + private var fontFamily: String? = null + private var fontWeight: Int? = null private val layoutCallback = Choreographer.FrameCallback { isLayoutEnqueued = false @@ -96,6 +104,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context if (icons.containsKey(index)) { menuItem.icon = getDrawable(icons[index]!!) } + if (item.badge.isNotEmpty()) { val badge = this.getOrCreateBadge(index) badge.isVisible = true @@ -112,6 +121,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context onTabSelected(menuItem) updateTintColors(menuItem) } + updateTextAppearance() } } } @@ -211,7 +221,55 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context hapticFeedbackEnabled = enabled } - fun emitHapticFeedback(feedbackConstants: Int) { + fun setFontSize(size: Int) { + fontSize = size + updateTextAppearance() + } + + fun setFontFamily(family: String?) { + fontFamily = family + updateTextAppearance() + } + + fun setFontWeight(weight: String?) { + val fontWeight = ReactTypefaceUtils.parseFontWeight(weight) + this.fontWeight = fontWeight + updateTextAppearance() + } + + private fun getTypefaceStyle(weight: Int?) = when (weight) { + 700 -> Typeface.BOLD + else -> Typeface.NORMAL + } + + private fun updateTextAppearance() { + if (fontSize != null || fontFamily != null || fontWeight != null) { + val menuView = getChildAt(0) as? ViewGroup ?: return + val size = fontSize?.toFloat()?.takeIf { it > 0 } ?: 12f + val typeface = ReactFontManager.getInstance().getTypeface( + fontFamily ?: "", + getTypefaceStyle(fontWeight), + context.assets + ) + + for (i in 0 until menuView.childCount) { + val item = menuView.getChildAt(i) + val largeLabel = + item.findViewById(com.google.android.material.R.id.navigation_bar_item_large_label_view) + val smallLabel = + item.findViewById(com.google.android.material.R.id.navigation_bar_item_small_label_view) + + listOf(largeLabel, smallLabel).forEach { label -> + label?.apply { + setTextSize(TypedValue.COMPLEX_UNIT_SP, size) + setTypeface(typeface) + } + } + } + } + } + + private fun emitHapticFeedback(feedbackConstants: Int) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && hapticFeedbackEnabled) { this.performHapticFeedback(feedbackConstants) } diff --git a/android/src/newarch/RCTTabViewManager.kt b/android/src/newarch/RCTTabViewManager.kt index af363c3f..a53c4905 100644 --- a/android/src/newarch/RCTTabViewManager.kt +++ b/android/src/newarch/RCTTabViewManager.kt @@ -127,6 +127,18 @@ class RCTTabViewManager(context: ReactApplicationContext) : ) } + override fun setFontFamily(view: ReactBottomNavigationView?, value: String?) { + view?.setFontFamily(value) + } + + override fun setFontWeight(view: ReactBottomNavigationView?, value: String?) { + view?.setFontWeight(value) + } + + override fun setFontSize(view: ReactBottomNavigationView?, value: Int) { + view?.setFontSize(value) + } + // iOS Methods override fun setTranslucent(view: ReactBottomNavigationView?, value: Boolean) { @@ -143,4 +155,6 @@ class RCTTabViewManager(context: ReactApplicationContext) : override fun setScrollEdgeAppearance(view: ReactBottomNavigationView?, value: String?) { } + + } diff --git a/android/src/oldarch/RCTTabViewManager.kt b/android/src/oldarch/RCTTabViewManager.kt index 75b028ba..bb631081 100644 --- a/android/src/oldarch/RCTTabViewManager.kt +++ b/android/src/oldarch/RCTTabViewManager.kt @@ -114,6 +114,21 @@ class RCTTabViewManager(context: ReactApplicationContext) : SimpleViewManager Appearance attributes for the tab bar when a scroll view is at the bottom. diff --git a/docs/docs/docs/guides/usage-with-react-navigation.mdx b/docs/docs/docs/guides/usage-with-react-navigation.mdx index 02e79fbd..6067d409 100644 --- a/docs/docs/docs/guides/usage-with-react-navigation.mdx +++ b/docs/docs/docs/guides/usage-with-react-navigation.mdx @@ -149,6 +149,16 @@ Tab views using the sidebar adaptable style have an appearance Whether to enable haptic feedback on tab press. Defaults to true. +#### `tabLabelStyle` + +Object containing styles for the tab label. + +Supported properties: +- `fontFamily` +- `fontSize` +- `fontWeight` + + ### Options The following options can be used to configure the screens in the navigator. These can be specified under `screenOptions` prop of `Tab.navigator` or `options` prop of `Tab.Screen`. diff --git a/example/src/Examples/NativeBottomTabs.tsx b/example/src/Examples/NativeBottomTabs.tsx index 0de6ea86..5970189a 100644 --- a/example/src/Examples/NativeBottomTabs.tsx +++ b/example/src/Examples/NativeBottomTabs.tsx @@ -18,6 +18,10 @@ function NativeBottomTabs() { tabBarActiveTintColor="#F7DBA7" barTintColor="#1E2D2F" rippleColor="#F7DBA7" + tabLabelStyle={{ + fontFamily: 'Avenir', + fontSize: 15, + }} activeIndicatorColor="#041F1E" screenListeners={{ tabLongPress: (data) => { diff --git a/ios/Fabric/RCTTabViewComponentView.mm b/ios/Fabric/RCTTabViewComponentView.mm index d735a724..c68cfc81 100644 --- a/ios/Fabric/RCTTabViewComponentView.mm +++ b/ios/Fabric/RCTTabViewComponentView.mm @@ -146,6 +146,18 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & if (oldViewProps.hapticFeedbackEnabled != newViewProps.hapticFeedbackEnabled) { _tabViewProvider.hapticFeedbackEnabled = newViewProps.hapticFeedbackEnabled; } + + if (oldViewProps.fontSize != newViewProps.fontSize) { + _tabViewProvider.fontSize = [NSNumber numberWithInt:newViewProps.fontSize]; + } + + if (oldViewProps.fontWeight != newViewProps.fontWeight) { + _tabViewProvider.fontWeigth = RCTNSStringFromStringNilIfEmpty(newViewProps.fontWeight); + } + + if (oldViewProps.fontFamily != newViewProps.fontFamily) { + _tabViewProvider.fontFamily = RCTNSStringFromStringNilIfEmpty(newViewProps.fontFamily); + } [super updateProps:props oldProps:oldProps]; } @@ -179,7 +191,12 @@ bool haveTabItemsChanged(const std::vector& oldItems, NSMutableArray *result = [NSMutableArray array]; for (const auto& item : items) { - auto tabInfo = [[TabInfo alloc] initWithKey:RCTNSStringFromString(item.key) title:RCTNSStringFromString(item.title) badge:RCTNSStringFromString(item.badge) sfSymbol:RCTNSStringFromString(item.sfSymbol) activeTintColor:RCTUIColorFromSharedColor(item.activeTintColor) hidden:item.hidden]; + auto tabInfo = [[TabInfo alloc] initWithKey:RCTNSStringFromString(item.key) + title:RCTNSStringFromString(item.title) + badge:RCTNSStringFromStringNilIfEmpty(item.badge) + sfSymbol:RCTNSStringFromStringNilIfEmpty(item.sfSymbol) + activeTintColor:RCTUIColorFromSharedColor(item.activeTintColor) + hidden:item.hidden]; [result addObject:tabInfo]; } diff --git a/ios/RCTTabViewViewManager.mm b/ios/RCTTabViewViewManager.mm index ba2bfb24..004a7b01 100644 --- a/ios/RCTTabViewViewManager.mm +++ b/ios/RCTTabViewViewManager.mm @@ -43,6 +43,9 @@ - (instancetype)init RCT_EXPORT_VIEW_PROPERTY(activeTintColor, UIColor) RCT_EXPORT_VIEW_PROPERTY(inactiveTintColor, UIColor) RCT_EXPORT_VIEW_PROPERTY(hapticFeedbackEnabled, BOOL) +RCT_EXPORT_VIEW_PROPERTY(fontFamily, NSString) +RCT_EXPORT_VIEW_PROPERTY(fontWeight, NSString) +RCT_EXPORT_VIEW_PROPERTY(fontSize, NSNumber) // MARK: TabViewProviderDelegate diff --git a/ios/TabViewImpl.swift b/ios/TabViewImpl.swift index 3b3bb468..8d0cfafc 100644 --- a/ios/TabViewImpl.swift +++ b/ios/TabViewImpl.swift @@ -22,6 +22,9 @@ class TabViewProps: ObservableObject { @Published var ignoresTopSafeArea: Bool = true @Published var disablePageAnimations: Bool = false @Published var hapticFeedbackEnabled: Bool = true + @Published var fontSize: Int? + @Published var fontFamily: String? + @Published var fontWeight: String? var selectedActiveTintColor: UIColor? { if let selectedPage = selectedPage, @@ -166,19 +169,70 @@ struct TabItem: View { } private func updateTabBarAppearance(props: TabViewProps, tabBar: UITabBar?) { - guard let tabBar else { return } - let appearanceType = props.scrollEdgeAppearance - - if (appearanceType == "transparent") { - tabBar.barTintColor = props.barTintColor - tabBar.isTranslucent = props.translucent - tabBar.unselectedItemTintColor = props.inactiveTintColor - return + guard let tabBar else { return } + + if props.scrollEdgeAppearance == "transparent" { + configureTransparentAppearance(tabBar: tabBar, props: props) + return + } + + configureStandardAppearance(tabBar: tabBar, props: props) +} + +private func createFontAttributes( + size: CGFloat, + family: String?, + weight: String?, + inactiveTintColor: UIColor? +) -> [NSAttributedString.Key: Any] { + var attributes: [NSAttributedString.Key: Any] = [:] + + if let inactiveTintColor { + attributes[.foregroundColor] = inactiveTintColor + } + + if family != nil || weight != nil { + attributes[.font] = RCTFont.update( + nil, + withFamily: family, + size: NSNumber(value: size), + weight: weight, + style: nil, + variant: nil, + scaleMultiplier: 1.0 + ) + } else { + attributes[.font] = UIFont.boldSystemFont(ofSize: size) + } + + return attributes +} + +private func configureTransparentAppearance(tabBar: UITabBar, props: TabViewProps) { + tabBar.barTintColor = props.barTintColor + tabBar.isTranslucent = props.translucent + tabBar.unselectedItemTintColor = props.inactiveTintColor + + guard let items = tabBar.items else { return } + + let fontSize = props.fontSize != nil ? CGFloat(props.fontSize!) : UIFont.smallSystemFontSize + let attributes = createFontAttributes( + size: fontSize, + family: props.fontFamily, + weight: props.fontWeight, + inactiveTintColor: props.inactiveTintColor + ) + + items.forEach { item in + item.setTitleTextAttributes(attributes, for: .normal) } +} +private func configureStandardAppearance(tabBar: UITabBar, props: TabViewProps) { let appearance = UITabBarAppearance() - - switch appearanceType { + + // Configure background + switch props.scrollEdgeAppearance { case "opaque": appearance.configureWithOpaqueBackground() default: @@ -186,16 +240,29 @@ private func updateTabBarAppearance(props: TabViewProps, tabBar: UITabBar?) { } appearance.backgroundColor = props.barTintColor + // Configure item appearance + let itemAppearance = UITabBarItemAppearance() + let fontSize = props.fontSize != nil ? CGFloat(props.fontSize!) : UIFont.smallSystemFontSize + + let attributes = createFontAttributes( + size: fontSize, + family: props.fontFamily, + weight: props.fontWeight, + inactiveTintColor: props.inactiveTintColor + ) + if let inactiveTintColor = props.inactiveTintColor { - let itemAppearance = UITabBarItemAppearance() itemAppearance.normal.iconColor = inactiveTintColor - itemAppearance.normal.titleTextAttributes = [.foregroundColor: inactiveTintColor] - - appearance.stackedLayoutAppearance = itemAppearance - appearance.inlineLayoutAppearance = itemAppearance - appearance.compactInlineLayoutAppearance = itemAppearance } + itemAppearance.normal.titleTextAttributes = attributes + + // Apply item appearance to all layouts + appearance.stackedLayoutAppearance = itemAppearance + appearance.inlineLayoutAppearance = itemAppearance + appearance.compactInlineLayoutAppearance = itemAppearance + + // Apply final appearance tabBar.standardAppearance = appearance if #available(iOS 15.0, *) { tabBar.scrollEdgeAppearance = appearance.copy() @@ -272,6 +339,15 @@ extension View { .onChange(of: props.selectedActiveTintColor) { newValue in updateTabBarAppearance(props: props, tabBar: tabBar) } + .onChange(of: props.fontSize) { newValue in + updateTabBarAppearance(props: props, tabBar: tabBar) + } + .onChange(of: props.fontFamily) { newValue in + updateTabBarAppearance(props: props, tabBar: tabBar) + } + .onChange(of: props.fontWeight) { newValue in + updateTabBarAppearance(props: props, tabBar: tabBar) + } } @ViewBuilder diff --git a/ios/TabViewProvider.swift b/ios/TabViewProvider.swift index 487e62f5..e2b2a100 100644 --- a/ios/TabViewProvider.swift +++ b/ios/TabViewProvider.swift @@ -130,6 +130,24 @@ import React props.inactiveTintColor = inactiveTintColor } } + + @objc public var fontFamily: NSString? { + didSet { + props.fontFamily = fontFamily as? String + } + } + + @objc public var fontWeigth: NSString? { + didSet { + props.fontWeight = fontWeigth as? String + } + } + + @objc public var fontSize: NSNumber? { + didSet { + props.fontSize = fontSize as? Int + } + } // New arch specific properties diff --git a/src/TabView.tsx b/src/TabView.tsx index 9d14c8b0..be888ba7 100644 --- a/src/TabView.tsx +++ b/src/TabView.tsx @@ -123,6 +123,22 @@ interface Props { * Color of tab indicator. (Android only) */ activeIndicatorColor?: ColorValue; + tabLabelStyle?: { + /** + * Font family for the tab labels. + */ + fontFamily?: string; + + /** + * Font weight for the tab labels. + */ + fontWeight?: string; + + /** + * Font size for the tab labels. + */ + fontSize?: number; + }; } const ANDROID_MAX_TABS = 6; @@ -148,6 +164,7 @@ const TabView = ({ getHidden = ({ route }: { route: Route }) => route.hidden, getActiveTintColor = ({ route }: { route: Route }) => route.activeTintColor, hapticFeedbackEnabled = true, + tabLabelStyle, ...props }: Props) => { // @ts-ignore @@ -238,6 +255,7 @@ const TabView = ({ return ( ('RNCTabView', {