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

Added support for barTintColor in android ios #55

Merged
merged 8 commits into from
Oct 21, 2024
29 changes: 29 additions & 0 deletions android/src/main/java/com/rcttabview/RCTTabView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ package com.rcttabview

import android.content.Context
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.util.TypedValue
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
Expand All @@ -25,6 +28,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
var items: MutableList<TabInfo>? = null
var onTabSelectedListener: ((WritableMap) -> Unit)? = null
private var isAnimating = false
private var barTintColor : Int? = null
okwasniewski marked this conversation as resolved.
Show resolved Hide resolved

private val layoutCallback = Choreographer.FrameCallback {
isLayoutEnqueued = false
Expand Down Expand Up @@ -140,4 +144,29 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
super.onDetachedFromWindow()
isAnimating = false
}

fun setBarTintColor(color: Int?) {
barTintColor = color
updateBackgroundColors()
}

private fun updateBackgroundColors() {
// Set the color, either using the active background color or a default color.
val backgroundColor = barTintColor ?: getDefaultColorFor(android.R.attr.colorPrimary) ?: return

// Apply the same color to both active and inactive states
val colorDrawable = ColorDrawable(backgroundColor)

itemBackground = colorDrawable
}


private fun getDefaultColorFor(baseColorThemeAttr: Int): Int? {
val value = TypedValue()
if (!context.theme.resolveAttribute(baseColorThemeAttr, value, true)) {
return null
}
val baseColor = AppCompatResources.getColorStateList(context, value.resourceId)
return baseColor.defaultColor
}
}
5 changes: 5 additions & 0 deletions android/src/main/java/com/rcttabview/RCTTabViewViewManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ class RCTTabViewViewManager :
view.setIcons(icons)
}

@ReactProp(name = "barTintColor")
fun setBarTintColor(view: ReactBottomNavigationView, color: Int?) {
view.setBarTintColor(color)
}

public override fun createViewInstance(context: ThemedReactContext): ReactBottomNavigationView {
eventDispatcher = context.getNativeModule(UIManagerModule::class.java)!!.eventDispatcher
val view = ReactBottomNavigationView(context)
Expand Down
4 changes: 4 additions & 0 deletions docs/docs/docs/guides/usage-with-react-navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ Default options to use for the screens in the navigator.

Whether to show labels in tabs. Defaults to true.

#### `barTintColor`

Background color of the tab bar.

#### `disablePageAnimations`

Whether to disable page animations between tabs. (iOS only)
Expand Down
8 changes: 8 additions & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
return <FourTabs scrollEdgeAppearance="transparent" />;
};

const FourTabsWithBarTintColor = () => {
return <FourTabs barTintColor={'#87CEEB'} />;
};

const examples = [
{ component: ThreeTabs, name: 'Three Tabs' },
{ component: FourTabs, name: 'Four Tabs' },
Expand All @@ -50,6 +54,10 @@
component: FourTabsTransparentScrollEdgeAppearance,
name: 'Four Tabs - Transparent scroll edge appearance',
},
{
component: FourTabsWithBarTintColor,
name: 'Four Tabs - Custom Background Color of Tabs',
},
{ component: NativeBottomTabs, name: 'Native Bottom Tabs' },
{ component: JSBottomTabs, name: 'JS Bottom Tabs' },
{
Expand Down Expand Up @@ -96,7 +104,7 @@
name="BottomTabs Example"
component={App}
options={{
headerRight: () => (

Check warning on line 107 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
4 changes: 4 additions & 0 deletions example/src/Examples/FourTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@ import { Article } from '../Screens/Article';
import { Albums } from '../Screens/Albums';
import { Contacts } from '../Screens/Contacts';
import { Chat } from '../Screens/Chat';
import { ColorValue } from 'react-native';

interface Props {
ignoresTopSafeArea?: boolean;
disablePageAnimations?: boolean;
scrollEdgeAppearance?: 'default' | 'opaque' | 'transparent';
barTintColor?: ColorValue;
}

export default function FourTabs({
ignoresTopSafeArea = false,
disablePageAnimations = false,
scrollEdgeAppearance = 'default',
barTintColor,
}: Props) {
const [index, setIndex] = useState(0);
const [routes] = useState([
Expand Down Expand Up @@ -59,6 +62,7 @@ export default function FourTabs({
navigationState={{ index, routes }}
onIndexChange={setIndex}
renderScene={renderScene}
barTintColor={barTintColor}
/>
);
}
1 change: 1 addition & 0 deletions ios/RCTTabViewViewManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,6 @@ - (UIView *)view
RCT_EXPORT_VIEW_PROPERTY(ignoresTopSafeArea, BOOL)
RCT_EXPORT_VIEW_PROPERTY(disablePageAnimations, BOOL)
RCT_EXPORT_VIEW_PROPERTY(scrollEdgeAppearance, NSString)
RCT_EXPORT_VIEW_PROPERTY(barTintColor, NSNumber)

@end
18 changes: 18 additions & 0 deletions ios/TabViewImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class TabViewProps: ObservableObject {
@Published var ignoresTopSafeArea: Bool?
@Published var disablePageAnimations: Bool = false
@Published var scrollEdgeAppearance: String?
@Published var barTintColor: UIColor?
}

/**
Expand Down Expand Up @@ -76,6 +77,9 @@ struct TabViewImpl: View {
UITabBar.appearance().scrollEdgeAppearance = configureAppearance(for: newValue ?? "")
}
}
.onAppear {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should use .onChange(of: props.barTintColor) to allow dynamic changing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the suggestion!

I initially tried using .onChange(of: props.barTintColor), but it doesn't seem to update the barTintColor as expected in my case. The color only reflects properly when using .onAppear.

To make sure the barTintColor reflects both on view load and during dynamic changes, I combined both .onAppear and .onChange(of:). This approach ensures that the UITabBar appearance is set correctly when the view first appears and updates dynamically when barTintColor changes.

Here is what I am going to change:

.onAppear {
    updateTabBarAppearance(with props.barTintColor)
}
.onChange(of: barTintColor) { newColor in
    updateTabBarAppearance(with newColor)
}

Is this okay? @okwasniewski

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If thats the case you can do a custom extension on the view, lets keep simillar logic contained:

extension View {
  @ViewBuilder
  func tabBarTintColor(enabled: Bool) -> some View {
    self
		.onAppear {
            // Do something
         }
       .onChange(of: barTintColor) { newColor in
    // Do something
      }
  }

updateTabBarAppearance(with: props.barTintColor)
}
}
}

Expand All @@ -94,6 +98,20 @@ private func configureAppearance(for appearanceType: String) -> UITabBarAppearan
return appearance
}

// Helper function to update the tab bar appearance
private func updateTabBarAppearance(with barTintColor: UIColor?) {

if #available(iOS 15.0, *) {
let appearance = UITabBarAppearance()

appearance.backgroundColor = barTintColor
UITabBar.appearance().standardAppearance = appearance
UITabBar.appearance().scrollEdgeAppearance = appearance
} else {
UITabBar.appearance().barTintColor = barTintColor
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't it work for newer OS versions? I didn't see deprecation in the docs.

This function should ideally only call UITabBar.appearance().barTintColor. Overriding the appearance one more time can cause issues with scrollEdgeApperance.

Copy link
Contributor Author

@shubhamguptadream11 shubhamguptadream11 Oct 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried with this approach as well: UITabBar.appearance().barTintColor on iOS 17.2 simulator.

One wierd thing happening here is when I reach end by scrolling till end barTintColor disappeared.

Attaching video for reference:

vii.mp4

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shubhamguptadream11 This looks like its related to scrollEdgeAppearance (which controls whether the tab bar should disappear when nothing is behind.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yess

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  private func updateTabBarAppearance(with barTintColor: UIColor?) {
      if (barTintColor != nil) {
          
          if #available(iOS 15.0, *) {
              let appearance = UITabBarAppearance()
              
              appearance.configureWithOpaqueBackground()
              appearance.backgroundColor = barTintColor
              
              UITabBar.appearance().standardAppearance = appearance
              UITabBar.appearance().scrollEdgeAppearance = appearance
          } else {
              UITabBar.appearance().barTintColor = barTintColor
          }
      }
  }

Why are we putting a check for iOS 15? Is barTintColor deprecated?

No, barTintColor is not deprecated. The issue I raised above requires setting scrollEdgeAppearance, which is available from iOS 15. To set this, we create a UITabBarAppearance(), making the direct use of barTintColor redundant. Instead, we use backgroundColor.

Why are we not only using barTintColor to set the tab bar's color?
This is due to the need to set scrollEdgeAppearance, as mentioned above.

Why do we check for nil for barTintColor?
We check for nil to ensure that barTintColor is applied only when the user passes it from the React Native side. This way, scrollEdgeAppearance is overridden only when barTintColor is being applied.

Let me know if you have any other suggestion.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh okay, now I get what you mean.

In this case we need to have one function which updates everything related to apperance.

private func configureAppearance(for appearanceType: String, appearance: UITabBarAppearance) -> UITabBarAppearance {
  switch appearanceType {
  case "opaque":
    appearance.configureWithOpaqueBackground()
  case "transparent":
    appearance.configureWithTransparentBackground()
  default:
    appearance.configureWithDefaultBackground()
  }
  
  return appearance
}

// Helper function to update the tab bar appearance
private func updateTabBarAppearance(props: TabViewProps) {
  if #available(iOS 15.0, *) {
    let appearance = UITabBarAppearance()
    UITabBar.appearance().scrollEdgeAppearance = configureAppearance(
      for: props.scrollEdgeAppearance ?? "",
      appearance: appearance
    )
    appearance.backgroundColor = props.barTintColor
    UITabBar.appearance().standardAppearance = appearance
  } else {
    UITabBar.appearance().barTintColor = props.barTintColor
  }

Then we can combine it together with onChange:

.onAppear {
      updateTabBarAppearance(props: props)
    }
    .onChange(of: props.barTintColor) { newValue in
      updateTabBarAppearance(props: props)
    }
    .onChange(of: props.scrollEdgeAppearance) { newValue in
      updateTabBarAppearance(props: props)
    }

This should also fix current bug with scrollEdgeAppearance because its not updated onAppear

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you rebase on top of main as I've merged your recent PR, we need to also include it here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@okwasniewski
I refactored the code to handle the onAppear and onChange logic for all TabView props in one place, simplifying the update logic. I’ve tested the changes across all three props—translucent, scrollEdgeAppearance, and barTintColor—with all their possible values to ensure everything is working as expected.

}
}

struct TabItem: View {
var title: String?
var icon: UIImage?
Expand Down
6 changes: 6 additions & 0 deletions ios/TabViewProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ struct TabData: Codable {
props.items = parseTabData(from: items)
}
}

@objc var barTintColor: NSNumber? {
didSet {
props.barTintColor = RCTConvert.uiColor(barTintColor)
}
}

@objc public convenience init(eventDispatcher: RCTEventDispatcherProtocol, imageLoader: RCTImageLoader) {
self.init()
Expand Down
16 changes: 15 additions & 1 deletion src/TabView.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import type { TabViewItems } from './TabViewNativeComponent';
import { Image, Platform, StyleSheet, View } from 'react-native';
import {
ColorValue,
Image,
Platform,
StyleSheet,
View,
processColor,
} from 'react-native';

//@ts-ignore
import type { ImageSource } from 'react-native/Libraries/Image/ImageSource';
Expand Down Expand Up @@ -77,6 +84,11 @@ interface Props<Route extends BaseRoute> {
route: Route;
focused: boolean;
}) => ImageSource | undefined;

/**
* Background color of the tab bar.
*/
barTintColor?: ColorValue;
}

const ANDROID_MAX_TABS = 6;
Expand All @@ -93,6 +105,7 @@ const TabView = <Route extends BaseRoute>({
? route.focusedIcon
: route.unfocusedIcon
: route.focusedIcon,
barTintColor,
...props
}: Props<Route>) => {
// @ts-ignore
Expand Down Expand Up @@ -179,6 +192,7 @@ const TabView = <Route extends BaseRoute>({
onPageSelected={({ nativeEvent: { key } }) => {
jumpTo(key);
}}
barTintColor={processColor(barTintColor)}
{...props}
>
{trimmedRoutes.map((route) => {
Expand Down
3 changes: 2 additions & 1 deletion src/TabViewNativeComponent.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
import type { ViewProps } from 'react-native';
import type { ProcessedColorValue, ViewProps } from 'react-native';
import type { DirectEventHandler } from 'react-native/Libraries/Types/CodegenTypes';
//@ts-ignore
import type { ImageSource } from 'react-native/Libraries/Image/ImageSource';
Expand All @@ -23,6 +23,7 @@ export interface TabViewProps extends ViewProps {
labeled?: boolean;
sidebarAdaptable?: boolean;
scrollEdgeAppearance?: string;
barTintColor?: ProcessedColorValue | null;
}

export default codegenNativeComponent<TabViewProps>('RCTTabView');
Loading