Skip to content

Commit d499559

Browse files
Adding Active Inactive Tint Color (#42)
* feat(ios): add active/inactive tint color * feat(ios): use active/inactive tint colors from navigation theme * feat(android): implement active/inactive tint colors * docs: update README and example to add tint colors * refactor(android): update `updateTintColors` to use more kotlin features * fix(ios): return self if no tint color pass to tintColor modifier * feat(android): add default values for checked/unchecked states * feat(ios): adding tint colors for items * fix(ios): appearance configuration for UITabBar colors * fix: make it work on iOS and Android, React Navigation fixes * docs: add new props * fix: rename props --------- Co-authored-by: Oskar Kwaśniewski <[email protected]>
1 parent 97c7aed commit d499559

19 files changed

+329
-37
lines changed

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

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
3030
var items: MutableList<TabInfo>? = null
3131
var onTabSelectedListener: ((WritableMap) -> Unit)? = null
3232
private var isAnimating = false
33+
private var activeTintColor: Int? = null
34+
private var inactiveTintColor: Int? = null
35+
private val checkedStateSet = intArrayOf(android.R.attr.state_checked)
36+
private val uncheckedStateSet = intArrayOf(-android.R.attr.state_checked)
3337

3438
private val layoutCallback = Choreographer.FrameCallback {
3539
isLayoutEnqueued = false
@@ -43,6 +47,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
4347
init {
4448
setOnItemSelectedListener { item ->
4549
onTabSelected(item)
50+
updateTintColors(item)
4651
true
4752
}
4853
}
@@ -160,12 +165,41 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
160165
itemBackground = colorDrawable
161166
}
162167

168+
fun setActiveTintColor(color: Int?) {
169+
activeTintColor = color
170+
updateTintColors()
171+
}
172+
173+
fun setInactiveTintColor(color: Int?) {
174+
inactiveTintColor = color
175+
updateTintColors()
176+
}
177+
178+
private fun updateTintColors(item: MenuItem? = null) {
179+
// First let's check current item color.
180+
val currentItemTintColor = items?.find { it.title == item?.title }?.activeTintColor
181+
182+
// getDeaultColor will always return a valid color but to satisfy the compiler we need to check for null
183+
val colorPrimary = currentItemTintColor ?: activeTintColor ?: getDefaultColorFor(android.R.attr.colorPrimary) ?: return
184+
val colorSecondary =
185+
inactiveTintColor ?: getDefaultColorFor(android.R.attr.textColorSecondary) ?: return
186+
val states = arrayOf(uncheckedStateSet, checkedStateSet)
187+
val colors = intArrayOf(colorSecondary, colorPrimary)
188+
189+
ColorStateList(states, colors).apply {
190+
this@ReactBottomNavigationView.itemTextColor = this
191+
this@ReactBottomNavigationView.itemIconTintList = this
192+
}
193+
}
194+
163195
private fun getDefaultColorFor(baseColorThemeAttr: Int): Int? {
164196
val value = TypedValue()
165197
if (!context.theme.resolveAttribute(baseColorThemeAttr, value, true)) {
166198
return null
167199
}
168-
val baseColor = AppCompatResources.getColorStateList(context, value.resourceId)
200+
val baseColor = AppCompatResources.getColorStateList(
201+
context, value.resourceId
202+
)
169203
return baseColor.defaultColor
170204
}
171205
}

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

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ import com.facebook.yoga.YogaNode
2121
data class TabInfo(
2222
val key: String,
2323
val title: String,
24-
val badge: String
24+
val badge: String,
25+
val activeTintColor: Int?
2526
)
2627

2728
@ReactModule(name = RCTTabViewViewManager.NAME)
@@ -42,7 +43,8 @@ class RCTTabViewViewManager :
4243
TabInfo(
4344
key = item.getString("key") ?: "",
4445
title = item.getString("title") ?: "",
45-
badge = item.getString("badge") ?: ""
46+
badge = item.getString("badge") ?: "",
47+
activeTintColor = if (item.hasKey("activeTintColor")) item.getInt("activeTintColor") else null
4648
)
4749
)
4850
}
@@ -57,7 +59,6 @@ class RCTTabViewViewManager :
5759
}
5860
}
5961

60-
6162
@ReactProp(name = "labeled")
6263
fun setLabeled(view: ReactBottomNavigationView, flag: Boolean?) {
6364
view.setLabeled(flag)
@@ -72,7 +73,7 @@ class RCTTabViewViewManager :
7273
fun setBarTintColor(view: ReactBottomNavigationView, color: Int?) {
7374
view.setBarTintColor(color)
7475
}
75-
76+
7677
@ReactProp(name = "rippleColor")
7778
fun setRippleColor(view: ReactBottomNavigationView, rippleColor: Int?) {
7879
if (rippleColor != null) {
@@ -81,8 +82,14 @@ class RCTTabViewViewManager :
8182
}
8283
}
8384

84-
@ReactProp(name = "translucent")
85-
fun setTranslucentview(view: ReactBottomNavigationView, translucent: Boolean?) {
85+
@ReactProp(name = "activeTintColor")
86+
fun setActiveTintColor(view: ReactBottomNavigationView, color: Int?) {
87+
view.setActiveTintColor(color)
88+
}
89+
90+
@ReactProp(name = "inactiveTintColor")
91+
fun setInactiveTintColor(view: ReactBottomNavigationView, color: Int?) {
92+
view.setInactiveTintColor(color)
8693
}
8794

8895
public override fun createViewInstance(context: ThemedReactContext): ReactBottomNavigationView {
@@ -96,7 +103,6 @@ class RCTTabViewViewManager :
96103
return view
97104
}
98105

99-
100106
class TabViewShadowNode() : LayoutShadowNode(),
101107
YogaMeasureFunction {
102108
private var mWidth = 0
@@ -161,4 +167,8 @@ class RCTTabViewViewManager :
161167
@ReactProp(name = "disablePageAnimations")
162168
fun setDisablePageAnimations(view: ReactBottomNavigationView, flag: Boolean) {
163169
}
170+
171+
@ReactProp(name = "translucent")
172+
fun setTranslucentview(view: ReactBottomNavigationView, translucent: Boolean?) {
173+
}
164174
}

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ Whether to disable page animations between tabs.
6565

6666
Describes the appearance attributes for the tabBar to use when an observable scroll view is scrolled to the bottom.
6767

68+
### `tabBarActiveTintColor`
69+
70+
Color for the active tab.
71+
72+
### `tabBarInactiveTintColor`
73+
74+
Color for the inactive tabs.
75+
6876
#### `barTintColor`
6977

7078
Background color of the tab bar.
@@ -106,6 +114,14 @@ Title text for the screen.
106114

107115
Label text of the tab displayed in the navigation bar. When undefined, scene title is used.
108116

117+
#### `tabBarActiveTintColor`
118+
119+
Color for the active tab.
120+
121+
:::note
122+
The `tabBarInactiveTintColor` is not supported on route level due to native limitations. Use `inactiveTintColor` in the `Tab.Navigator` instead.
123+
:::
124+
109125
#### `tabBarIcon`
110126

111127
Function that given `{ focused: boolean }` returns `ImageSource` or `AppleIcon` to display in the navigation bar.

example/src/App.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,14 @@ import {
1010
TouchableOpacity,
1111
Button,
1212
Alert,
13+
useColorScheme,
1314
} from 'react-native';
14-
import { NavigationContainer, useNavigation } from '@react-navigation/native';
15+
import {
16+
DarkTheme,
17+
DefaultTheme,
18+
NavigationContainer,
19+
useNavigation,
20+
} from '@react-navigation/native';
1521
import { createStackNavigator } from '@react-navigation/stack';
1622
import { createNativeStackNavigator } from '@react-navigation/native-stack';
1723
import { SafeAreaProvider } from 'react-native-safe-area-context';
@@ -22,6 +28,7 @@ import MaterialBottomTabs from './Examples/MaterialBottomTabs';
2228
import SFSymbols from './Examples/SFSymbols';
2329
import LabeledTabs from './Examples/Labeled';
2430
import NativeBottomTabs from './Examples/NativeBottomTabs';
31+
import TintColorsExample from './Examples/TintColors';
2532

2633
const FourTabsIgnoreSafeArea = () => {
2734
return <FourTabs ignoresTopSafeArea />;
@@ -82,6 +89,7 @@ const examples = [
8289
screenOptions: { headerShown: false },
8390
},
8491
{ component: MaterialBottomTabs, name: 'Material (JS) Bottom Tabs' },
92+
{ component: TintColorsExample, name: 'Tint Colors' },
8593
];
8694

8795
function App() {
@@ -112,9 +120,12 @@ const NativeStack = createNativeStackNavigator();
112120
export default function Navigation() {
113121
const [mode, setMode] = React.useState<'native' | 'js'>('native');
114122
const NavigationStack = mode === 'js' ? Stack : NativeStack;
123+
const colorScheme = useColorScheme();
124+
const theme = colorScheme === 'dark' ? DarkTheme : DefaultTheme;
125+
115126
return (
116127
<SafeAreaProvider>
117-
<NavigationContainer>
128+
<NavigationContainer theme={theme}>
118129
<NavigationStack.Navigator initialRouteName="BottomTabs Example">
119130
<NavigationStack.Screen
120131
name="BottomTabs Example"

example/src/Examples/NativeBottomTabs.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const Tab = createNativeBottomTabNavigator();
99

1010
function NativeBottomTabs() {
1111
return (
12-
<Tab.Navigator>
12+
<Tab.Navigator tabBarInactiveTintColor="red" tabBarActiveTintColor="orange">
1313
<Tab.Screen
1414
name="Article"
1515
component={Article}
@@ -33,13 +33,15 @@ function NativeBottomTabs() {
3333
component={Contacts}
3434
options={{
3535
tabBarIcon: () => require('../../assets/icons/person_dark.png'),
36+
tabBarActiveTintColor: 'yellow',
3637
}}
3738
/>
3839
<Tab.Screen
3940
name="Chat"
4041
component={Chat}
4142
options={{
4243
tabBarIcon: () => require('../../assets/icons/chat_dark.png'),
44+
tabBarActiveTintColor: 'purple',
4345
}}
4446
/>
4547
</Tab.Navigator>
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import TabView, { SceneMap } from 'react-native-bottom-tabs';
2+
import { useState } from 'react';
3+
import { Article } from '../Screens/Article';
4+
import { Albums } from '../Screens/Albums';
5+
import { Contacts } from '../Screens/Contacts';
6+
import { Chat } from '../Screens/Chat';
7+
8+
export default function TintColorsExample() {
9+
const [index, setIndex] = useState(0);
10+
const [routes] = useState([
11+
{
12+
key: 'article',
13+
title: 'Article',
14+
focusedIcon: require('../../assets/icons/article_dark.png'),
15+
unfocusedIcon: require('../../assets/icons/chat_dark.png'),
16+
badge: '!',
17+
},
18+
{
19+
key: 'albums',
20+
title: 'Albums',
21+
focusedIcon: require('../../assets/icons/grid_dark.png'),
22+
badge: '5',
23+
activeTintColor: 'green',
24+
},
25+
{
26+
key: 'contacts',
27+
focusedIcon: require('../../assets/icons/person_dark.png'),
28+
title: 'Contacts',
29+
activeTintColor: 'yellow',
30+
},
31+
{
32+
key: 'chat',
33+
focusedIcon: require('../../assets/icons/chat_dark.png'),
34+
title: 'Chat',
35+
},
36+
]);
37+
38+
const renderScene = SceneMap({
39+
article: Article,
40+
albums: Albums,
41+
contacts: Contacts,
42+
chat: Chat,
43+
});
44+
45+
return (
46+
<TabView
47+
sidebarAdaptable
48+
navigationState={{ index, routes }}
49+
onIndexChange={setIndex}
50+
renderScene={renderScene}
51+
tabBarActiveTintColor="red"
52+
tabBarInactiveTintColor="orange"
53+
scrollEdgeAppearance="default"
54+
/>
55+
);
56+
}

ios/Extensions.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ extension Collection {
88
}
99
}
1010

11+
12+
extension Collection where Element == TabInfo {
13+
func findByKey(_ key: String?) -> Element? {
14+
guard let key else { return nil }
15+
guard !isEmpty else { return nil }
16+
return first(where: { $0.key == key })
17+
}
18+
}
19+
1120
extension UIView {
1221
func pinEdges(to other: UIView) {
1322
NSLayoutConstraint.activate([

ios/RCTTabViewViewManager.mm

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,7 @@ - (UIView *)view
3434
RCT_EXPORT_VIEW_PROPERTY(scrollEdgeAppearance, NSString)
3535
RCT_EXPORT_VIEW_PROPERTY(barTintColor, NSNumber)
3636
RCT_EXPORT_VIEW_PROPERTY(translucent, BOOL)
37+
RCT_EXPORT_VIEW_PROPERTY(activeTintColor, NSNumber)
38+
RCT_EXPORT_VIEW_PROPERTY(inactiveTintColor, NSNumber)
3739

3840
@end

0 commit comments

Comments
 (0)