Skip to content

Commit 0307fe7

Browse files
authored
feat: add custom fonts support (#135)
* feat: add fonts * fix: change api
1 parent b6b6ad0 commit 0307fe7

File tree

12 files changed

+264
-18
lines changed

12 files changed

+264
-18
lines changed

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

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.rcttabview
22

33
import android.content.Context
44
import android.content.res.ColorStateList
5+
import android.graphics.Typeface
56
import android.graphics.drawable.BitmapDrawable
67
import android.graphics.drawable.ColorDrawable
78
import android.graphics.drawable.Drawable
@@ -11,6 +12,8 @@ import android.view.Choreographer
1112
import android.view.HapticFeedbackConstants
1213
import android.view.MenuItem
1314
import android.view.View
15+
import android.view.ViewGroup
16+
import android.widget.TextView
1417
import androidx.appcompat.content.res.AppCompatResources
1518
import com.facebook.common.references.CloseableReference
1619
import com.facebook.datasource.DataSources
@@ -20,8 +23,10 @@ import com.facebook.imagepipeline.request.ImageRequestBuilder
2023
import com.facebook.react.bridge.Arguments
2124
import com.facebook.react.bridge.ReadableArray
2225
import com.facebook.react.bridge.WritableMap
26+
import com.facebook.react.common.assets.ReactFontManager
2327
import com.facebook.react.modules.core.ReactChoreographer
2428
import com.facebook.react.views.imagehelper.ImageSource
29+
import com.facebook.react.views.text.ReactTypefaceUtils
2530
import com.google.android.material.bottomnavigation.BottomNavigationView
2631

2732

@@ -37,6 +42,9 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
3742
private val checkedStateSet = intArrayOf(android.R.attr.state_checked)
3843
private val uncheckedStateSet = intArrayOf(-android.R.attr.state_checked)
3944
private var hapticFeedbackEnabled = true
45+
private var fontSize: Int? = null
46+
private var fontFamily: String? = null
47+
private var fontWeight: Int? = null
4048

4149
private val layoutCallback = Choreographer.FrameCallback {
4250
isLayoutEnqueued = false
@@ -96,6 +104,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
96104
if (icons.containsKey(index)) {
97105
menuItem.icon = getDrawable(icons[index]!!)
98106
}
107+
99108
if (item.badge.isNotEmpty()) {
100109
val badge = this.getOrCreateBadge(index)
101110
badge.isVisible = true
@@ -112,6 +121,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
112121
onTabSelected(menuItem)
113122
updateTintColors(menuItem)
114123
}
124+
updateTextAppearance()
115125
}
116126
}
117127
}
@@ -211,7 +221,55 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
211221
hapticFeedbackEnabled = enabled
212222
}
213223

214-
fun emitHapticFeedback(feedbackConstants: Int) {
224+
fun setFontSize(size: Int) {
225+
fontSize = size
226+
updateTextAppearance()
227+
}
228+
229+
fun setFontFamily(family: String?) {
230+
fontFamily = family
231+
updateTextAppearance()
232+
}
233+
234+
fun setFontWeight(weight: String?) {
235+
val fontWeight = ReactTypefaceUtils.parseFontWeight(weight)
236+
this.fontWeight = fontWeight
237+
updateTextAppearance()
238+
}
239+
240+
private fun getTypefaceStyle(weight: Int?) = when (weight) {
241+
700 -> Typeface.BOLD
242+
else -> Typeface.NORMAL
243+
}
244+
245+
private fun updateTextAppearance() {
246+
if (fontSize != null || fontFamily != null || fontWeight != null) {
247+
val menuView = getChildAt(0) as? ViewGroup ?: return
248+
val size = fontSize?.toFloat()?.takeIf { it > 0 } ?: 12f
249+
val typeface = ReactFontManager.getInstance().getTypeface(
250+
fontFamily ?: "",
251+
getTypefaceStyle(fontWeight),
252+
context.assets
253+
)
254+
255+
for (i in 0 until menuView.childCount) {
256+
val item = menuView.getChildAt(i)
257+
val largeLabel =
258+
item.findViewById<TextView>(com.google.android.material.R.id.navigation_bar_item_large_label_view)
259+
val smallLabel =
260+
item.findViewById<TextView>(com.google.android.material.R.id.navigation_bar_item_small_label_view)
261+
262+
listOf(largeLabel, smallLabel).forEach { label ->
263+
label?.apply {
264+
setTextSize(TypedValue.COMPLEX_UNIT_SP, size)
265+
setTypeface(typeface)
266+
}
267+
}
268+
}
269+
}
270+
}
271+
272+
private fun emitHapticFeedback(feedbackConstants: Int) {
215273
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && hapticFeedbackEnabled) {
216274
this.performHapticFeedback(feedbackConstants)
217275
}

android/src/newarch/RCTTabViewManager.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,18 @@ class RCTTabViewManager(context: ReactApplicationContext) :
127127
)
128128
}
129129

130+
override fun setFontFamily(view: ReactBottomNavigationView?, value: String?) {
131+
view?.setFontFamily(value)
132+
}
133+
134+
override fun setFontWeight(view: ReactBottomNavigationView?, value: String?) {
135+
view?.setFontWeight(value)
136+
}
137+
138+
override fun setFontSize(view: ReactBottomNavigationView?, value: Int) {
139+
view?.setFontSize(value)
140+
}
141+
130142
// iOS Methods
131143

132144
override fun setTranslucent(view: ReactBottomNavigationView?, value: Boolean) {
@@ -143,4 +155,6 @@ class RCTTabViewManager(context: ReactApplicationContext) :
143155

144156
override fun setScrollEdgeAppearance(view: ReactBottomNavigationView?, value: String?) {
145157
}
158+
159+
146160
}

android/src/oldarch/RCTTabViewManager.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,21 @@ class RCTTabViewManager(context: ReactApplicationContext) : SimpleViewManager<Re
114114
tabViewImpl.setHapticFeedbackEnabled(view, value)
115115
}
116116

117+
@ReactProp(name = "fontFamily")
118+
fun setFontFamily(view: ReactBottomNavigationView?, value: String?) {
119+
view?.setFontFamily(value)
120+
}
121+
122+
@ReactProp(name = "fontWeight")
123+
fun setFontWeight(view: ReactBottomNavigationView?, value: String?) {
124+
view?.setFontWeight(value)
125+
}
126+
127+
@ReactProp(name = "fontSize")
128+
fun setFontSize(view: ReactBottomNavigationView?, value: Int) {
129+
view?.setFontSize(value)
130+
}
131+
117132
class TabViewShadowNode() : LayoutShadowNode(),
118133
YogaMeasureFunction {
119134
private var mWidth = 0

docs/docs/docs/guides/standalone-usage.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,15 @@ Whether to enable haptic feedback on tab press.
133133
- Type: `boolean`
134134
- Default: `true`
135135

136+
137+
#### `tabLabelStyle`
138+
139+
Object containing styles for the tab label.
140+
Supported properties:
141+
- `fontFamily`
142+
- `fontSize`
143+
- `fontWeight`
144+
136145
#### `scrollEdgeAppearance` <Badge text="iOS" type="info" />
137146

138147
Appearance attributes for the tab bar when a scroll view is at the bottom.

docs/docs/docs/guides/usage-with-react-navigation.mdx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,16 @@ Tab views using the sidebar adaptable style have an appearance
149149

150150
Whether to enable haptic feedback on tab press. Defaults to true.
151151

152+
#### `tabLabelStyle`
153+
154+
Object containing styles for the tab label.
155+
156+
Supported properties:
157+
- `fontFamily`
158+
- `fontSize`
159+
- `fontWeight`
160+
161+
152162
### Options
153163

154164
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`.

example/src/Examples/NativeBottomTabs.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ function NativeBottomTabs() {
1818
tabBarActiveTintColor="#F7DBA7"
1919
barTintColor="#1E2D2F"
2020
rippleColor="#F7DBA7"
21+
tabLabelStyle={{
22+
fontFamily: 'Avenir',
23+
fontSize: 15,
24+
}}
2125
activeIndicatorColor="#041F1E"
2226
screenListeners={{
2327
tabLongPress: (data) => {

ios/Fabric/RCTTabViewComponentView.mm

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,18 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
146146
if (oldViewProps.hapticFeedbackEnabled != newViewProps.hapticFeedbackEnabled) {
147147
_tabViewProvider.hapticFeedbackEnabled = newViewProps.hapticFeedbackEnabled;
148148
}
149+
150+
if (oldViewProps.fontSize != newViewProps.fontSize) {
151+
_tabViewProvider.fontSize = [NSNumber numberWithInt:newViewProps.fontSize];
152+
}
153+
154+
if (oldViewProps.fontWeight != newViewProps.fontWeight) {
155+
_tabViewProvider.fontWeigth = RCTNSStringFromStringNilIfEmpty(newViewProps.fontWeight);
156+
}
157+
158+
if (oldViewProps.fontFamily != newViewProps.fontFamily) {
159+
_tabViewProvider.fontFamily = RCTNSStringFromStringNilIfEmpty(newViewProps.fontFamily);
160+
}
149161

150162
[super updateProps:props oldProps:oldProps];
151163
}
@@ -179,7 +191,12 @@ bool haveTabItemsChanged(const std::vector<RNCTabViewItemsStruct>& oldItems,
179191
NSMutableArray<TabInfo *> *result = [NSMutableArray array];
180192

181193
for (const auto& item : items) {
182-
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];
194+
auto tabInfo = [[TabInfo alloc] initWithKey:RCTNSStringFromString(item.key)
195+
title:RCTNSStringFromString(item.title)
196+
badge:RCTNSStringFromStringNilIfEmpty(item.badge)
197+
sfSymbol:RCTNSStringFromStringNilIfEmpty(item.sfSymbol)
198+
activeTintColor:RCTUIColorFromSharedColor(item.activeTintColor)
199+
hidden:item.hidden];
183200

184201
[result addObject:tabInfo];
185202
}

ios/RCTTabViewViewManager.mm

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ - (instancetype)init
4343
RCT_EXPORT_VIEW_PROPERTY(activeTintColor, UIColor)
4444
RCT_EXPORT_VIEW_PROPERTY(inactiveTintColor, UIColor)
4545
RCT_EXPORT_VIEW_PROPERTY(hapticFeedbackEnabled, BOOL)
46+
RCT_EXPORT_VIEW_PROPERTY(fontFamily, NSString)
47+
RCT_EXPORT_VIEW_PROPERTY(fontWeight, NSString)
48+
RCT_EXPORT_VIEW_PROPERTY(fontSize, NSNumber)
4649

4750
// MARK: TabViewProviderDelegate
4851

ios/TabViewImpl.swift

Lines changed: 92 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ class TabViewProps: ObservableObject {
2222
@Published var ignoresTopSafeArea: Bool = true
2323
@Published var disablePageAnimations: Bool = false
2424
@Published var hapticFeedbackEnabled: Bool = true
25+
@Published var fontSize: Int?
26+
@Published var fontFamily: String?
27+
@Published var fontWeight: String?
2528

2629
var selectedActiveTintColor: UIColor? {
2730
if let selectedPage = selectedPage,
@@ -171,36 +174,100 @@ struct TabItem: View {
171174
}
172175

173176
private func updateTabBarAppearance(props: TabViewProps, tabBar: UITabBar?) {
174-
guard let tabBar else { return }
175-
let appearanceType = props.scrollEdgeAppearance
176-
177-
if (appearanceType == "transparent") {
178-
tabBar.barTintColor = props.barTintColor
179-
tabBar.isTranslucent = props.translucent
180-
tabBar.unselectedItemTintColor = props.inactiveTintColor
181-
return
177+
guard let tabBar else { return }
178+
179+
if props.scrollEdgeAppearance == "transparent" {
180+
configureTransparentAppearance(tabBar: tabBar, props: props)
181+
return
182+
}
183+
184+
configureStandardAppearance(tabBar: tabBar, props: props)
185+
}
186+
187+
private func createFontAttributes(
188+
size: CGFloat,
189+
family: String?,
190+
weight: String?,
191+
inactiveTintColor: UIColor?
192+
) -> [NSAttributedString.Key: Any] {
193+
var attributes: [NSAttributedString.Key: Any] = [:]
194+
195+
if let inactiveTintColor {
196+
attributes[.foregroundColor] = inactiveTintColor
197+
}
198+
199+
if family != nil || weight != nil {
200+
attributes[.font] = RCTFont.update(
201+
nil,
202+
withFamily: family,
203+
size: NSNumber(value: size),
204+
weight: weight,
205+
style: nil,
206+
variant: nil,
207+
scaleMultiplier: 1.0
208+
)
209+
} else {
210+
attributes[.font] = UIFont.boldSystemFont(ofSize: size)
211+
}
212+
213+
return attributes
214+
}
215+
216+
private func configureTransparentAppearance(tabBar: UITabBar, props: TabViewProps) {
217+
tabBar.barTintColor = props.barTintColor
218+
tabBar.isTranslucent = props.translucent
219+
tabBar.unselectedItemTintColor = props.inactiveTintColor
220+
221+
guard let items = tabBar.items else { return }
222+
223+
let fontSize = props.fontSize != nil ? CGFloat(props.fontSize!) : UIFont.smallSystemFontSize
224+
let attributes = createFontAttributes(
225+
size: fontSize,
226+
family: props.fontFamily,
227+
weight: props.fontWeight,
228+
inactiveTintColor: props.inactiveTintColor
229+
)
230+
231+
items.forEach { item in
232+
item.setTitleTextAttributes(attributes, for: .normal)
182233
}
234+
}
183235

236+
private func configureStandardAppearance(tabBar: UITabBar, props: TabViewProps) {
184237
let appearance = UITabBarAppearance()
185-
186-
switch appearanceType {
238+
239+
// Configure background
240+
switch props.scrollEdgeAppearance {
187241
case "opaque":
188242
appearance.configureWithOpaqueBackground()
189243
default:
190244
appearance.configureWithDefaultBackground()
191245
}
192246
appearance.backgroundColor = props.barTintColor
193247

248+
// Configure item appearance
249+
let itemAppearance = UITabBarItemAppearance()
250+
let fontSize = props.fontSize != nil ? CGFloat(props.fontSize!) : UIFont.smallSystemFontSize
251+
252+
let attributes = createFontAttributes(
253+
size: fontSize,
254+
family: props.fontFamily,
255+
weight: props.fontWeight,
256+
inactiveTintColor: props.inactiveTintColor
257+
)
258+
194259
if let inactiveTintColor = props.inactiveTintColor {
195-
let itemAppearance = UITabBarItemAppearance()
196260
itemAppearance.normal.iconColor = inactiveTintColor
197-
itemAppearance.normal.titleTextAttributes = [.foregroundColor: inactiveTintColor]
198-
199-
appearance.stackedLayoutAppearance = itemAppearance
200-
appearance.inlineLayoutAppearance = itemAppearance
201-
appearance.compactInlineLayoutAppearance = itemAppearance
202261
}
203262

263+
itemAppearance.normal.titleTextAttributes = attributes
264+
265+
// Apply item appearance to all layouts
266+
appearance.stackedLayoutAppearance = itemAppearance
267+
appearance.inlineLayoutAppearance = itemAppearance
268+
appearance.compactInlineLayoutAppearance = itemAppearance
269+
270+
// Apply final appearance
204271
tabBar.standardAppearance = appearance
205272
if #available(iOS 15.0, *) {
206273
tabBar.scrollEdgeAppearance = appearance.copy()
@@ -277,6 +344,15 @@ extension View {
277344
.onChange(of: props.selectedActiveTintColor) { newValue in
278345
updateTabBarAppearance(props: props, tabBar: tabBar)
279346
}
347+
.onChange(of: props.fontSize) { newValue in
348+
updateTabBarAppearance(props: props, tabBar: tabBar)
349+
}
350+
.onChange(of: props.fontFamily) { newValue in
351+
updateTabBarAppearance(props: props, tabBar: tabBar)
352+
}
353+
.onChange(of: props.fontWeight) { newValue in
354+
updateTabBarAppearance(props: props, tabBar: tabBar)
355+
}
280356
}
281357

282358
@ViewBuilder

0 commit comments

Comments
 (0)