feat: add support for badges to bottom tab bar

This commit is contained in:
Satyajit Sahoo
2020-07-10 21:11:54 +02:00
parent e63580edbe
commit 96c7b688ce
9 changed files with 137 additions and 14 deletions

View File

@@ -97,6 +97,7 @@ export default function BottomTabsScreen({
options={{
tabBarLabel: 'Chat',
tabBarIcon: getTabBarIcon('message-reply'),
tabBarBadge: 2,
}}
/>
<BottomTabs.Screen

View File

@@ -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.

View 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',
},
});

View File

@@ -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}

View File

@@ -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',

View File

@@ -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%',
},
});

View File

@@ -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)',
},
};

View File

@@ -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)',
},
};

View File

@@ -13,6 +13,7 @@ export type Theme = {
card: string;
text: string;
border: string;
notification: string;
};
};