Skip to content

Commit 9c720a7

Browse files
committed
feat(mobile): enhance tab bar icon and label animations
- Implemented new TabIcon and TextLabel components for improved tab bar interactions - Added fade and color interpolation animations for tab icons and labels - Extracted icon and label rendering logic from main Tabbar component - Introduced new animation configurations for tab bar elements Signed-off-by: Innei <tukon479@gmail.com>
1 parent ba02009 commit 9c720a7

File tree

1 file changed

+119
-38
lines changed

1 file changed

+119
-38
lines changed

apps/mobile/src/components/ui/tabbar/Tabbar.tsx

Lines changed: 119 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
import type { BottomTabBarProps } from "@react-navigation/bottom-tabs"
2-
import { TabBarIcon } from "@react-navigation/bottom-tabs/src/views/TabBarIcon"
32
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"
104
import type { FC } from "react"
115
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"
1516
import { useSafeAreaInsets } from "react-native-safe-area-context"
1617

1718
import { SetBottomTabBarHeightContext } from "@/src/components/ui/tabbar/contexts/BottomTabBarHeightContext"
@@ -27,7 +28,6 @@ export const Tabbar: FC<BottomTabBarProps> = (props) => {
2728
const { state, navigation, descriptors } = props
2829
const { routes } = state
2930
const setTabBarHeight = useContext(SetBottomTabBarHeightContext)
30-
const { fonts } = useTheme()
3131

3232
const insets = useSafeAreaInsets()
3333
const tabBarVisible = useContext(BottomTabBarVisibleContext)
@@ -85,23 +85,13 @@ export const Tabbar: FC<BottomTabBarProps> = (props) => {
8585
: undefined
8686

8787
const renderIcon = ({ focused }: { focused: boolean }) => {
88-
const activeOpacity = focused ? 1 : 0
89-
const inactiveOpacity = focused ? 0 : 1
90-
88+
const iconSize = ICON_SIZE_ROUND
9189
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}
10293
inactiveTintColor={inactiveTintColor}
10394
renderIcon={options.tabBarIcon!}
104-
style={options.tabBarIconStyle as StyleProp<ViewStyle>}
10595
/>
10696
)
10797
}
@@ -127,20 +117,13 @@ export const Tabbar: FC<BottomTabBarProps> = (props) => {
127117
}
128118

129119
return (
130-
<Text
131-
numberOfLines={1}
120+
<TextLabel
121+
focused={focused}
132122
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+
/>
144127
)
145128
}
146129
const scene = { route, focused }
@@ -162,6 +145,89 @@ export const Tabbar: FC<BottomTabBarProps> = (props) => {
162145
</Animated.View>
163146
)
164147
}
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
165231
const styles = StyleSheet.create({
166232
labelBeneath: {
167233
fontSize: 10,
@@ -172,6 +238,21 @@ const styles = StyleSheet.create({
172238
backgroundColor: "transparent",
173239
borderTopWidth: StyleSheet.hairlineWidth,
174240
},
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+
},
175256
})
176257

177258
const AnimatedThemedBlurView = Animated.createAnimatedComponent(ThemedBlurView)

0 commit comments

Comments
 (0)