Skip to content

Commit

Permalink
feat: add bottom tabs animation
Browse files Browse the repository at this point in the history
  • Loading branch information
teneeto committed Apr 5, 2023
1 parent 96de131 commit 030227b
Show file tree
Hide file tree
Showing 13 changed files with 264 additions and 238 deletions.
28 changes: 0 additions & 28 deletions example/src/Screens/BottomTabs.tsx
Expand Up @@ -74,34 +74,6 @@ export function BottomTabs({
<HeaderBackButton {...props} onPress={navigation.goBack} />
),
}}
sceneAnimationOptions={{
animationEnabled: true,
animationType: 'shifting',
transitionSpec: {
animation: 'timing',
config: {
duration: 1000,
},
},
sceneStyleInterpolator: ({ current }) => {
const top = current.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
});

const left = current.interpolate({
inputRange: [-1, 0, 1],
outputRange: [-50, 0, 50],
});

return {
sceneStyle: {
opacty: 1,
transform: [{ translateX: left }, { translateY: top }],
},
};
},
}}
>
<Tab.Screen
name="TabStack"
Expand Down
143 changes: 143 additions & 0 deletions example/src/Screens/BottomTabsAnimated.tsx
@@ -0,0 +1,143 @@
import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons';
import {
createBottomTabNavigator,
SceneStyleInterpolator,
TransitionSpecs,
useBottomTabBarHeight,
} from '@react-navigation/bottom-tabs';
import { HeaderBackButton, useHeaderHeight } from '@react-navigation/elements';
import {
getFocusedRouteNameFromRoute,
NavigatorScreenParams,
ParamListBase,
useIsFocused,
} from '@react-navigation/native';
import type { StackScreenProps } from '@react-navigation/stack';
import { BlurView } from 'expo-blur';
import * as React from 'react';
import { ScrollView, StatusBar, StyleSheet } from 'react-native';

import { Albums } from '../Shared/Albums';
import { Chat } from '../Shared/Chat';
import { Contacts } from '../Shared/Contacts';
import { SimpleStack, SimpleStackParams } from './SimpleStack';

const getTabBarIcon =
(name: React.ComponentProps<typeof MaterialCommunityIcons>['name']) =>
({ color, size }: { color: string; size: number }) =>
<MaterialCommunityIcons name={name} color={color} size={size} />;

type BottomTabParams = {
TabStack: NavigatorScreenParams<SimpleStackParams>;
TabAlbums: undefined;
TabContacts: undefined;
TabChat: undefined;
};

const AlbumsScreen = () => {
const headerHeight = useHeaderHeight();
const tabBarHeight = useBottomTabBarHeight();
const isFocused = useIsFocused();

return (
<>
{isFocused && <StatusBar barStyle="light-content" />}
<ScrollView
contentContainerStyle={{
paddingTop: headerHeight,
paddingBottom: tabBarHeight,
}}
>
<Albums scrollEnabled={false} />
</ScrollView>
</>
);
};

const Tab = createBottomTabNavigator<BottomTabParams>();

export function BottomTabsAnimated({
navigation,
route,
}: StackScreenProps<ParamListBase, string>) {
const routeName = getFocusedRouteNameFromRoute(route) ?? 'Article';

React.useLayoutEffect(() => {
navigation.setOptions({
headerShown: false,
title: routeName,
});
}, [navigation, routeName]);

return (
<Tab.Navigator
screenOptions={{
headerLeft: (props) => (
<HeaderBackButton {...props} onPress={navigation.goBack} />
),
sceneAnimationOptions: {
animationEnabled: true,
transitionSpec: TransitionSpecs.CrossFadeAnimationSpec,
styleInterpolator: ({ current }) =>
SceneStyleInterpolator.forCrossFade({ current }),
},
}}
>
<Tab.Screen
name="TabStack"
component={SimpleStack}
options={{
title: 'Article',
tabBarIcon: getTabBarIcon('file-document'),
}}
/>
<Tab.Screen
name="TabChat"
component={Chat}
options={{
tabBarLabel: 'Chat',
tabBarIcon: getTabBarIcon('message-reply'),
tabBarBadge: 2,
}}
/>
<Tab.Screen
name="TabContacts"
component={Contacts}
options={{
title: 'Contacts',
tabBarIcon: getTabBarIcon('contacts'),
}}
/>
<Tab.Screen
name="TabAlbums"
component={AlbumsScreen}
options={{
title: 'Albums',
headerTintColor: '#fff',
headerTransparent: true,
headerBackground: () => (
<BlurView
tint="dark"
intensity={100}
style={StyleSheet.absoluteFill}
/>
),
tabBarIcon: getTabBarIcon('image-album'),
tabBarInactiveTintColor: 'rgba(255, 255, 255, 0.5)',
tabBarActiveTintColor: '#fff',
tabBarStyle: {
position: 'absolute',
borderTopColor: 'rgba(0, 0, 0, .2)',
},
tabBarBackground: () => (
<BlurView
tint="dark"
intensity={100}
style={StyleSheet.absoluteFill}
/>
),
}}
/>
</Tab.Navigator>
);
}
5 changes: 5 additions & 0 deletions example/src/screens.tsx
Expand Up @@ -2,6 +2,7 @@ import type { NavigatorScreenParams } from '@react-navigation/native';

import { AuthFlow } from './Screens/AuthFlow';
import { BottomTabs } from './Screens/BottomTabs';
import { BottomTabsAnimated } from './Screens/BottomTabsAnimated';
import { DrawerView } from './Screens/DrawerView';
import { DynamicTabs } from './Screens/DynamicTabs';
import { LinkComponent } from './Screens/LinkComponent';
Expand Down Expand Up @@ -66,6 +67,10 @@ export const SCREENS = {
title: 'Bottom Tabs',
component: BottomTabs,
},
BottomTabsAnimated: {
title: 'Bottom Tabs Animated',
component: BottomTabsAnimated,
},
MaterialTopTabs: {
title: 'Material Top Tabs',
component: MaterialTopTabsScreen,
Expand Down
@@ -0,0 +1,20 @@
import type {
BottomTabSceneInterpolatedStyle,
BottomTabSceneInterpolationProps,
} from '../types';

/**
* Simple cross fade animation
*/
export function forCrossFade({
current,
}: BottomTabSceneInterpolationProps): BottomTabSceneInterpolatedStyle {
return {
sceneStyle: {
opacity: current.interpolate({
inputRange: [-1, 0, 1],
outputRange: [0, 1, 0],
}),
},
};
}
11 changes: 11 additions & 0 deletions packages/bottom-tabs/src/TransitionConfigs/TransitionSpecs.tsx
@@ -0,0 +1,11 @@
import { Easing } from 'react-native';

import type { TransitionSpec } from '../types';

export const CrossFadeAnimationSpec: TransitionSpec = {
animation: 'timing',
config: {
duration: 400,
easing: Easing.ease,
},
};
7 changes: 6 additions & 1 deletion packages/bottom-tabs/src/__tests__/index.test.tsx
@@ -1,11 +1,16 @@
import { NavigationContainer, ParamListBase } from '@react-navigation/native';
import { fireEvent, render } from '@testing-library/react-native';
import * as React from 'react';
import { Button, Text, View } from 'react-native';
import { Animated, Button, Text, View } from 'react-native';

import { BottomTabScreenProps, createBottomTabNavigator } from '../index';

it('renders a bottom tab navigator with screens', async () => {
// @ts-expect-error: incomplete mock for testing
jest.spyOn(Animated, 'timing').mockImplementation(() => ({
start: (callback) => callback?.({ finished: true }),
}));

const Test = ({ route, navigation }: BottomTabScreenProps<ParamListBase>) => (
<View>
<Text>Screen {route.name}</Text>
Expand Down
8 changes: 8 additions & 0 deletions packages/bottom-tabs/src/index.tsx
@@ -1,3 +1,11 @@
import * as SceneStyleInterpolator from './TransitionConfigs/SceneStyleInterpolators';
import * as TransitionSpecs from './TransitionConfigs/TransitionSpecs';

/**
* Transition Presets
*/
export { SceneStyleInterpolator, TransitionSpecs };

/**
* Navigators
*/
Expand Down
55 changes: 20 additions & 35 deletions packages/bottom-tabs/src/types.tsx
Expand Up @@ -11,7 +11,6 @@ import type {
import type * as React from 'react';
import type {
Animated,
EasingFunction,
GestureResponderEvent,
StyleProp,
TextStyle,
Expand Down Expand Up @@ -255,6 +254,25 @@ export type BottomTabNavigationOptions = HeaderOptions & {
* Only supported on iOS and Android.
*/
freezeOnBlur?: boolean;

sceneAnimationOptions?: {
/**
* Whether default transition animation should be enabled on the screen.
* If you set it to `false`, the screen won't animate when pushing or popping.
* Defaults to `true` on Android and iOS, `false` on Web.
*/
animationEnabled?: boolean;

/**
* Function which specifies interpolated styles for various parts of the card, e.g. the overlay, shadow etc.
*/
styleInterpolator?: BottomTabSceneStyleInterpolator;

/**
* Object which specifies the animation type (timing or spring) and their options (such as duration for timing).
*/
transitionSpec?: TransitionSpec;
};
};

export type BottomTabDescriptor = Descriptor<
Expand All @@ -279,7 +297,7 @@ export type BottomTabSceneInterpolatedStyle = {
sceneStyle: any;
};

export type BottomTabCardStyleInterpolator = (
export type BottomTabSceneStyleInterpolator = (
props: BottomTabSceneInterpolationProps
) => BottomTabSceneInterpolatedStyle;

Expand Down Expand Up @@ -324,39 +342,6 @@ export type BottomTabNavigationConfig = {
* Style object for the component wrapping the screen content.
*/
sceneContainerStyle?: StyleProp<ViewStyle>;

/**
* various options to enable/disable and customize your scene transitions
*/
sceneAnimationOptions?: {
/**
* Whether default transition animation should be enabled on the screen.
* If you set it to `false`, the screen won't animate when pushing or popping.
* Defaults to `true` on Android and iOS, `false` on Web.
*/
animationEnabled?: boolean;

/**
* The scene animation Easing.
*/
animationEasing?: EasingFunction | undefined;

/**
* The scene animation effect. Specify `'shifting'` for a different effect.
* By default, 'opacity' will be used.
*/
animationType?: 'opacity' | 'shifting';

/**
* Function which specifies interpolated styles for various parts of the card, e.g. the overlay, shadow etc.
*/
sceneStyleInterpolator?: BottomTabCardStyleInterpolator;

/**
* Object which specifies the animation type (timing or spring) and their options (such as duration for timing).
*/
transitionSpec?: TransitionSpec;
};
};

export type BottomTabHeaderProps = {
Expand Down
13 changes: 0 additions & 13 deletions packages/bottom-tabs/src/utils/useAnimatedValueArray.tsx

This file was deleted.

17 changes: 17 additions & 0 deletions packages/bottom-tabs/src/utils/useAnimatedValueObject.tsx
@@ -0,0 +1,17 @@
import type { NavigationRoute, ParamListBase } from '@react-navigation/routers';
import * as React from 'react';
import { Animated } from 'react-native';

export function useAnimatedValueObject(
routes: NavigationRoute<ParamListBase, string>[]
) {
const refs = React.useRef<Record<string, Animated.Value>>({});
const previous = refs.current;
refs.current = {};

routes.forEach(({ key }) => {
refs.current[key] = previous[key] ?? new Animated.Value(0);
});

return refs.current;
}

0 comments on commit 030227b

Please sign in to comment.