Skip to content
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
60 changes: 59 additions & 1 deletion android/src/main/java/com/rcttabview/RCTTabView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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


Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -112,6 +121,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
onTabSelected(menuItem)
updateTintColors(menuItem)
}
updateTextAppearance()
}
}
}
Expand Down Expand Up @@ -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<TextView>(com.google.android.material.R.id.navigation_bar_item_large_label_view)
val smallLabel =
item.findViewById<TextView>(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)
}
Expand Down
14 changes: 14 additions & 0 deletions android/src/newarch/RCTTabViewManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -143,4 +155,6 @@ class RCTTabViewManager(context: ReactApplicationContext) :

override fun setScrollEdgeAppearance(view: ReactBottomNavigationView?, value: String?) {
}


}
15 changes: 15 additions & 0 deletions android/src/oldarch/RCTTabViewManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,21 @@ class RCTTabViewManager(context: ReactApplicationContext) : SimpleViewManager<Re
tabViewImpl.setHapticFeedbackEnabled(view, value)
}

@ReactProp(name = "fontFamily")
fun setFontFamily(view: ReactBottomNavigationView?, value: String?) {
view?.setFontFamily(value)
}

@ReactProp(name = "fontWeight")
fun setFontWeight(view: ReactBottomNavigationView?, value: String?) {
view?.setFontWeight(value)
}

@ReactProp(name = "fontSize")
fun setFontSize(view: ReactBottomNavigationView?, value: Int) {
view?.setFontSize(value)
}

class TabViewShadowNode() : LayoutShadowNode(),
YogaMeasureFunction {
private var mWidth = 0
Expand Down
9 changes: 9 additions & 0 deletions docs/docs/docs/guides/standalone-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,15 @@ Whether to enable haptic feedback on tab press.
- Type: `boolean`
- Default: `true`


#### `tabLabelStyle`

Object containing styles for the tab label.
Supported properties:
- `fontFamily`
- `fontSize`
- `fontWeight`

#### `scrollEdgeAppearance` <Badge text="iOS" type="info" />

Appearance attributes for the tab bar when a scroll view is at the bottom.
Expand Down
10 changes: 10 additions & 0 deletions docs/docs/docs/guides/usage-with-react-navigation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
4 changes: 4 additions & 0 deletions example/src/Examples/NativeBottomTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
tabBarActiveTintColor="#F7DBA7"
barTintColor="#1E2D2F"
rippleColor="#F7DBA7"
tabLabelStyle={{

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

View workflow job for this annotation

GitHub Actions / lint

Inline style: { fontFamily: 'Avenir', fontSize: 15 }
fontFamily: 'Avenir',
fontSize: 15,
}}
activeIndicatorColor="#041F1E"
screenListeners={{
tabLongPress: (data) => {
Expand Down
19 changes: 18 additions & 1 deletion ios/Fabric/RCTTabViewComponentView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
Expand Down Expand Up @@ -179,7 +191,12 @@ bool haveTabItemsChanged(const std::vector<RNCTabViewItemsStruct>& oldItems,
NSMutableArray<TabInfo *> *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];
}
Expand Down
3 changes: 3 additions & 0 deletions ios/RCTTabViewViewManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,20 @@
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

- (void)onLongPressWithKey:(NSString *)key reactTag:(NSNumber *)reactTag {
auto event = [[TabLongPressEvent alloc] initWithReactTag:reactTag key:key coalescingKey:_coalescingKey++];
[self.bridge.eventDispatcher sendEvent:event];

Check warning on line 54 in ios/RCTTabViewViewManager.mm

View workflow job for this annotation

GitHub Actions / build-ios

incompatible pointer types sending 'TabLongPressEvent *' to parameter of type 'id<RCTEvent>' [-Wincompatible-pointer-types]

Check warning on line 54 in ios/RCTTabViewViewManager.mm

View workflow job for this annotation

GitHub Actions / build-ios

incompatible pointer types sending 'TabLongPressEvent *' to parameter of type 'id<RCTEvent>' [-Wincompatible-pointer-types]
}

- (void)onPageSelectedWithKey:(NSString *)key reactTag:(NSNumber *)reactTag {
auto event = [[PageSelectedEvent alloc] initWithReactTag:reactTag key:key coalescingKey:_coalescingKey++];
[self.bridge.eventDispatcher sendEvent:event];

Check warning on line 59 in ios/RCTTabViewViewManager.mm

View workflow job for this annotation

GitHub Actions / build-ios

incompatible pointer types sending 'PageSelectedEvent *' to parameter of type 'id<RCTEvent>' [-Wincompatible-pointer-types]

Check warning on line 59 in ios/RCTTabViewViewManager.mm

View workflow job for this annotation

GitHub Actions / build-ios

incompatible pointer types sending 'PageSelectedEvent *' to parameter of type 'id<RCTEvent>' [-Wincompatible-pointer-types]
}

- (UIView *)view
Expand Down
108 changes: 92 additions & 16 deletions ios/TabViewImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -166,36 +169,100 @@ 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:
appearance.configureWithDefaultBackground()
}
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()
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading