1
1
import type { BottomTabBarProps } from "@react-navigation/bottom-tabs"
2
- import { TabBarIcon } from "@react-navigation/bottom-tabs/src/views/TabBarIcon"
3
2
import { getLabel } from "@react-navigation/elements"
4
- import {
5
- CommonActions ,
6
- NavigationContext ,
7
- NavigationRouteContext ,
8
- useTheme ,
9
- } from "@react-navigation/native"
3
+ import { CommonActions , NavigationContext , NavigationRouteContext } from "@react-navigation/native"
10
4
import type { FC } from "react"
11
5
import { useContext , useEffect } from "react"
12
- import type { StyleProp , ViewStyle } from "react-native"
13
- import { Platform , Pressable , StyleSheet , Text } from "react-native"
14
- import Animated , { useAnimatedStyle , useSharedValue , withSpring } from "react-native-reanimated"
6
+ import type { StyleProp , TextStyle } from "react-native"
7
+ import { Platform , Pressable , StyleSheet , View } from "react-native"
8
+ import Animated , {
9
+ FadeIn ,
10
+ FadeOut ,
11
+ interpolateColor ,
12
+ useAnimatedStyle ,
13
+ useSharedValue ,
14
+ withSpring ,
15
+ } from "react-native-reanimated"
15
16
import { useSafeAreaInsets } from "react-native-safe-area-context"
16
17
17
18
import { SetBottomTabBarHeightContext } from "@/src/components/ui/tabbar/contexts/BottomTabBarHeightContext"
@@ -27,7 +28,6 @@ export const Tabbar: FC<BottomTabBarProps> = (props) => {
27
28
const { state, navigation, descriptors } = props
28
29
const { routes } = state
29
30
const setTabBarHeight = useContext ( SetBottomTabBarHeightContext )
30
- const { fonts } = useTheme ( )
31
31
32
32
const insets = useSafeAreaInsets ( )
33
33
const tabBarVisible = useContext ( BottomTabBarVisibleContext )
@@ -85,23 +85,13 @@ export const Tabbar: FC<BottomTabBarProps> = (props) => {
85
85
: undefined
86
86
87
87
const renderIcon = ( { focused } : { focused : boolean } ) => {
88
- const activeOpacity = focused ? 1 : 0
89
- const inactiveOpacity = focused ? 0 : 1
90
-
88
+ const iconSize = ICON_SIZE_ROUND
91
89
return (
92
- < TabBarIcon
93
- route = { route }
94
- variant = { "uikit" }
95
- size = { "regular" }
96
- badge = { undefined }
97
- badgeStyle = { undefined }
98
- activeOpacity = { activeOpacity }
99
- allowFontScaling = { true }
100
- inactiveOpacity = { inactiveOpacity }
101
- activeTintColor = { accentColor }
90
+ < TabIcon
91
+ focused = { focused }
92
+ iconSize = { iconSize }
102
93
inactiveTintColor = { inactiveTintColor }
103
94
renderIcon = { options . tabBarIcon ! }
104
- style = { options . tabBarIconStyle as StyleProp < ViewStyle > }
105
95
/>
106
96
)
107
97
}
@@ -127,20 +117,13 @@ export const Tabbar: FC<BottomTabBarProps> = (props) => {
127
117
}
128
118
129
119
return (
130
- < Text
131
- numberOfLines = { 1 }
120
+ < TextLabel
121
+ focused = { focused }
132
122
accessibilityLabel = { accessibilityLabel }
133
- style = { StyleSheet . flatten ( [
134
- styles . labelBeneath ,
135
- fonts . regular ,
136
- {
137
- color : focused ? accentColor : inactiveTintColor ,
138
- } ,
139
- ] ) }
140
- allowFontScaling
141
- >
142
- { label }
143
- </ Text >
123
+ label = { label }
124
+ inactiveTintColor = { inactiveTintColor }
125
+ style = { styles . labelBeneath }
126
+ />
144
127
)
145
128
}
146
129
const scene = { route, focused }
@@ -162,6 +145,89 @@ export const Tabbar: FC<BottomTabBarProps> = (props) => {
162
145
</ Animated . View >
163
146
)
164
147
}
148
+
149
+ const tabIconIconAnimations = {
150
+ in : FadeIn . springify ( ) ,
151
+ out : FadeOut . springify ( ) ,
152
+ }
153
+
154
+ const TextLabel = ( props : {
155
+ focused : boolean
156
+ accessibilityLabel : string | undefined
157
+ label : string
158
+ inactiveTintColor : string
159
+ style : StyleProp < TextStyle >
160
+ } ) => {
161
+ const { focused, accessibilityLabel, label, inactiveTintColor, style } = props
162
+
163
+ const focusedValue = useSharedValue ( focused ? 1 : 0 )
164
+ const animatedStyle = useAnimatedStyle ( ( ) => ( {
165
+ ...styles . labelBeneath ,
166
+ color : interpolateColor ( focusedValue . value , [ 0 , 1 ] , [ inactiveTintColor , accentColor ] ) ,
167
+ } ) )
168
+ useEffect ( ( ) => {
169
+ focusedValue . value = withSpring ( focused ? 1 : 0 )
170
+ } , [ focused , focusedValue ] )
171
+ return (
172
+ < Animated . Text
173
+ numberOfLines = { 1 }
174
+ accessibilityLabel = { accessibilityLabel }
175
+ style = { StyleSheet . flatten ( [ style , animatedStyle ] ) }
176
+ allowFontScaling
177
+ >
178
+ { label }
179
+ </ Animated . Text >
180
+ )
181
+ }
182
+ const TabIcon = ( {
183
+ focused,
184
+ iconSize,
185
+ inactiveTintColor,
186
+ renderIcon,
187
+ } : {
188
+ focused : boolean
189
+ iconSize : number
190
+ inactiveTintColor : string
191
+ renderIcon : ( options : { focused : boolean ; size : number ; color : string } ) => React . ReactNode
192
+ } ) => {
193
+ const activeOpacity = focused ? 1 : 0
194
+ const inactiveOpacity = focused ? 0 : 1
195
+ return (
196
+ < View style = { styles . wrapperUikit } >
197
+ { focused && (
198
+ < Animated . View
199
+ entering = { tabIconIconAnimations . in }
200
+ exiting = { tabIconIconAnimations . out }
201
+ style = { [ styles . icon , { opacity : activeOpacity } ] }
202
+ >
203
+ { renderIcon ( {
204
+ focused : true ,
205
+ size : iconSize ,
206
+ color : accentColor ,
207
+ } ) }
208
+ </ Animated . View >
209
+ ) }
210
+ { ! focused && (
211
+ < Animated . View
212
+ entering = { tabIconIconAnimations . in }
213
+ exiting = { tabIconIconAnimations . out }
214
+ style = { [ styles . icon , { opacity : inactiveOpacity } ] }
215
+ >
216
+ { renderIcon ( {
217
+ focused : false ,
218
+ size : iconSize ,
219
+ color : inactiveTintColor ,
220
+ } ) }
221
+ </ Animated . View >
222
+ ) }
223
+ </ View >
224
+ )
225
+ }
226
+
227
+ // @copy node_modules/@react-navigation/bottom-tabs/src/views/TabBarIcon.tsx
228
+ const ICON_SIZE_WIDE = 31
229
+ const ICON_SIZE_TALL = 28
230
+ const ICON_SIZE_ROUND = 25
165
231
const styles = StyleSheet . create ( {
166
232
labelBeneath : {
167
233
fontSize : 10 ,
@@ -172,6 +238,21 @@ const styles = StyleSheet.create({
172
238
backgroundColor : "transparent" ,
173
239
borderTopWidth : StyleSheet . hairlineWidth ,
174
240
} ,
241
+ icon : {
242
+ // We render the icon twice at the same position on top of each other:
243
+ // active and inactive one, so we can fade between them:
244
+ // Cover the whole iconContainer:
245
+ position : "absolute" ,
246
+ alignSelf : "center" ,
247
+ alignItems : "center" ,
248
+ justifyContent : "center" ,
249
+ height : "100%" ,
250
+ width : "100%" ,
251
+ } ,
252
+ wrapperUikit : {
253
+ width : ICON_SIZE_WIDE ,
254
+ height : ICON_SIZE_TALL ,
255
+ } ,
175
256
} )
176
257
177
258
const AnimatedThemedBlurView = Animated . createAnimatedComponent ( ThemedBlurView )
0 commit comments