mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-04-28 12:25:21 +08:00
feat: add support for badges to bottom tab bar
This commit is contained in:
@@ -97,6 +97,7 @@ export default function BottomTabsScreen({
|
||||
options={{
|
||||
tabBarLabel: 'Chat',
|
||||
tabBarIcon: getTabBarIcon('message-reply'),
|
||||
tabBarBadge: 2,
|
||||
}}
|
||||
/>
|
||||
<BottomTabs.Screen
|
||||
|
||||
@@ -79,6 +79,11 @@ export type BottomTabNavigationOptions = {
|
||||
size: number;
|
||||
}) => React.ReactNode;
|
||||
|
||||
/**
|
||||
* Text to show in a badge on the tab icon.
|
||||
*/
|
||||
tabBarBadge?: number | string;
|
||||
|
||||
/**
|
||||
* Accessibility label for the tab button. This is read by the screen reader when the user taps the tab.
|
||||
* It's recommended to set this if you don't have a label for the tab.
|
||||
|
||||
83
packages/bottom-tabs/src/views/Badge.tsx
Normal file
83
packages/bottom-tabs/src/views/Badge.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import * as React from 'react';
|
||||
import { Animated, StyleSheet, StyleProp, TextStyle } from 'react-native';
|
||||
import color from 'color';
|
||||
import { useTheme } from '@react-navigation/native';
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* Whether the badge is visible
|
||||
*/
|
||||
visible: boolean;
|
||||
/**
|
||||
* Content of the `Badge`.
|
||||
*/
|
||||
children?: string | number;
|
||||
/**
|
||||
* Size of the `Badge`.
|
||||
*/
|
||||
size?: number;
|
||||
/**
|
||||
* Style object for the tab bar container.
|
||||
*/
|
||||
style?: Animated.WithAnimatedValue<StyleProp<TextStyle>>;
|
||||
};
|
||||
|
||||
export default function Badge({
|
||||
visible = true,
|
||||
size = 18,
|
||||
children,
|
||||
style,
|
||||
...rest
|
||||
}: Props) {
|
||||
const [opacity] = React.useState(() => new Animated.Value(visible ? 1 : 0));
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
React.useEffect(() => {
|
||||
Animated.timing(opacity, {
|
||||
toValue: visible ? 1 : 0,
|
||||
duration: 150,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
}, [opacity, visible]);
|
||||
|
||||
// @ts-expect-error: backgroundColor definitely exists
|
||||
const { backgroundColor = theme.colors.notification, ...restStyle } =
|
||||
StyleSheet.flatten(style) || {};
|
||||
const textColor = color(backgroundColor).isLight() ? 'black' : 'white';
|
||||
|
||||
const borderRadius = size / 2;
|
||||
const fontSize = Math.floor((size * 3) / 4);
|
||||
|
||||
return (
|
||||
<Animated.Text
|
||||
numberOfLines={1}
|
||||
style={[
|
||||
{
|
||||
opacity,
|
||||
backgroundColor,
|
||||
color: textColor,
|
||||
fontSize,
|
||||
lineHeight: size - 1,
|
||||
height: size,
|
||||
minWidth: size,
|
||||
borderRadius,
|
||||
},
|
||||
styles.container,
|
||||
restStyle,
|
||||
]}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</Animated.Text>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
alignSelf: 'flex-end',
|
||||
textAlign: 'center',
|
||||
paddingHorizontal: 4,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
});
|
||||
@@ -245,6 +245,7 @@ export default function BottomTabBar({
|
||||
inactiveBackgroundColor={inactiveBackgroundColor}
|
||||
button={options.tabBarButton}
|
||||
icon={options.tabBarIcon}
|
||||
badge={options.tabBarBadge}
|
||||
label={label}
|
||||
showLabel={showLabel}
|
||||
labelStyle={labelStyle}
|
||||
|
||||
@@ -39,6 +39,10 @@ type Props = {
|
||||
size: number;
|
||||
color: string;
|
||||
}) => React.ReactNode;
|
||||
/**
|
||||
* Text to show in a badge on the tab icon.
|
||||
*/
|
||||
badge?: number | string;
|
||||
/**
|
||||
* URL to use for the link to the tab.
|
||||
*/
|
||||
@@ -113,6 +117,7 @@ export default function BottomTabBarItem({
|
||||
route,
|
||||
label,
|
||||
icon,
|
||||
badge,
|
||||
to,
|
||||
button = ({
|
||||
children,
|
||||
@@ -220,16 +225,14 @@ export default function BottomTabBarItem({
|
||||
return (
|
||||
<TabBarIcon
|
||||
route={route}
|
||||
size={horizontal ? 17 : 24}
|
||||
horizontal={horizontal}
|
||||
badge={badge}
|
||||
activeOpacity={activeOpacity}
|
||||
inactiveOpacity={inactiveOpacity}
|
||||
activeTintColor={activeTintColor}
|
||||
inactiveTintColor={inactiveTintColor}
|
||||
renderIcon={icon}
|
||||
style={[
|
||||
horizontal ? styles.iconHorizontal : styles.iconVertical,
|
||||
iconStyle,
|
||||
]}
|
||||
style={iconStyle}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -276,12 +279,6 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
iconVertical: {
|
||||
flex: 1,
|
||||
},
|
||||
iconHorizontal: {
|
||||
height: '100%',
|
||||
},
|
||||
label: {
|
||||
textAlign: 'center',
|
||||
backgroundColor: 'transparent',
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React from 'react';
|
||||
import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native';
|
||||
import type { Route } from '@react-navigation/native';
|
||||
import Badge from './Badge';
|
||||
|
||||
type Props = {
|
||||
route: Route<string>;
|
||||
size: number;
|
||||
horizontal: boolean;
|
||||
badge?: string | number;
|
||||
activeOpacity: number;
|
||||
inactiveOpacity: number;
|
||||
activeTintColor: string;
|
||||
@@ -18,18 +20,23 @@ type Props = {
|
||||
};
|
||||
|
||||
export default function TabBarIcon({
|
||||
horizontal,
|
||||
badge,
|
||||
activeOpacity,
|
||||
inactiveOpacity,
|
||||
activeTintColor,
|
||||
inactiveTintColor,
|
||||
renderIcon,
|
||||
size,
|
||||
style,
|
||||
}: Props) {
|
||||
const size = horizontal ? 17 : 24;
|
||||
|
||||
// We render the icon twice at the same position on top of each other:
|
||||
// active and inactive one, so we can fade between them.
|
||||
return (
|
||||
<View style={style}>
|
||||
<View
|
||||
style={[horizontal ? styles.iconHorizontal : styles.iconVertical, style]}
|
||||
>
|
||||
<View style={[styles.icon, { opacity: activeOpacity }]}>
|
||||
{renderIcon({
|
||||
focused: true,
|
||||
@@ -44,6 +51,16 @@ export default function TabBarIcon({
|
||||
color: inactiveTintColor,
|
||||
})}
|
||||
</View>
|
||||
<Badge
|
||||
visible={badge != null}
|
||||
style={[
|
||||
styles.badge,
|
||||
horizontal ? styles.badgeHorizontal : styles.badgeVertical,
|
||||
]}
|
||||
size={(size * 3) / 4}
|
||||
>
|
||||
{badge}
|
||||
</Badge>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -62,4 +79,20 @@ const styles = StyleSheet.create({
|
||||
// Workaround for react-native >= 0.54 layout bug
|
||||
minWidth: 25,
|
||||
},
|
||||
iconVertical: {
|
||||
flex: 1,
|
||||
},
|
||||
iconHorizontal: {
|
||||
height: '100%',
|
||||
},
|
||||
badge: {
|
||||
position: 'absolute',
|
||||
left: 3,
|
||||
},
|
||||
badgeVertical: {
|
||||
top: 3,
|
||||
},
|
||||
badgeHorizontal: {
|
||||
bottom: '50%',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ const DarkTheme: Theme = {
|
||||
card: 'rgb(18, 18, 18)',
|
||||
text: 'rgb(229, 229, 231)',
|
||||
border: 'rgb(39, 39, 41)',
|
||||
notification: 'rgb(255, 69, 58)',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ const DefaultTheme: Theme = {
|
||||
card: 'rgb(255, 255, 255)',
|
||||
text: 'rgb(28, 28, 30)',
|
||||
border: 'rgb(224, 224, 224)',
|
||||
notification: 'rgb(255, 59, 48)',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ export type Theme = {
|
||||
card: string;
|
||||
text: string;
|
||||
border: string;
|
||||
notification: string;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user