Customising React Navigation: Implement WhatsApp animated header

Nowadays, I’m trying to update React Native development skills by cloning WhatsApp app. By resorting to a various of tutorials from Internet, I finished a couple of interfaces and features easily, util doing the header animation as following:

WhatsApp header animation

Header could slide up and down automatically by sliding left and right gestures. Unfortunately, React Navigation doesn’t provide enough configuration options for this effect. However, I notice that the indicator executes a similar animation while sliding left and right gestures happen.

Analysis

According to the source code of TabBarIndicator.tsx, the customisation basically includes:

  1. TabBar.tsx — — Where does the animation happen?

The navigator created by createMaterialTopTabNavigator is actually the MaterialTopTabView component which has a tabBar prop. The following source code shows tabBar will be rendered as a MaterialTopTabBar by default.

// MaterialTopTabView.tsxexport default function MaterialTopTabView({...tabBar = (props: MaterialTopTabBarProps) => <MaterialTopTabBar {...props} />,
...
return (<NavigationHelpersContext.Provider value={navigation}> <TabView
...
// TabView.tsx
...
render(){
...
renderTabBar({...sceneRendererProps, navigationState,})}
...
// MaterialTopTabBar.tsxexport default function TabBarTop(props: MaterialTopTabBarProps)

...
return (
<TabBar
...

The TabView will render MaterialTopTabBar via renderTabBar function which will call tabBar function defined in MaterialTopTabView. However, MaterialTopTabBar here is just a wrapper which extracts the props for its child component — TabBar. Therefore, TabBar is where the animation happens while sliding, not the MaterialTopTabBar.

2. Position — — What drives the animation happens by gestures?

As is known, the Indicator will execute an animation while sliding gestures happens. The following code shows the translation process along X-axis.

/** TabBarIndicator.tsx*/private getTranslateX = memoize(
...
// input Range is the page index starts from 0. Hers is [0,1,2,3]
const inputRange = routes.map((_, i) => i);
// output Range is the offset along x-axis of each tab item.
const outputRange = routes.reduce<number[]>((acc, _, i) => {
if (i === 0) return [0];
return [...acc, acc[i - 1] + getTabWidth(i - 1)];
}, []);
// position here is a prop from its parent component Pager, we just know its value will change based on the translate value and layout width.
const translateX = interpolate(position, {
inputRange,
outputRange,
extrapolate: Extrapolate.CLAMP,
});
return multiply(translateX, I18nManager.isRTL ? -1 : 1);
});
// in render function, calculate translateX and do transform animation.
...
render(){
...
const translateX = routes.length > 1 ? this.getTranslateX(position, routes, getTabWidth) : 0;
...
return(
<Animated.View
style={[
...
{ transform: [{ translateX }] as any }
...

The code explains how the indicator translate along x-axis while sliding gestures happen. After the calculation, the translateX will be valued by the current position and interpolate samples (inputRange and outputRange). Render function will finally update the new value by transform .The question now is simplified to calculate translateY while position is from 0 to 1. Actually the calculation will be done in UI thread directly to reduce the render time. React-Native-Reanimated is in charge of this process. We just need to define the calculation logic by the functions it provides. More details please view reanimated doc.

3.TranslateY — How to define the animation according to the gestures?

When position changes from 0 to 1, we just need to translate a distance which equals TabBar’s height along y-axis. Therefore, a custom header above TabBar will used here instead of the Stack Navigator header. Otherwise, the Stack navigator will not translate along with the TabBar.

4.Injection — How to take the customisation into effect?

According to the analysis of 1. Besides the TabBar.tsx file, We still need to define a new MaterialTopTabBar.tsx embedding the TabBar and being set to tabBar prop of the Navigator.

Solution

The tow files can be instantly created from the copy of MaterialTopTabBar.tsx and TabBar.tsx. The followings are the required modification to eliminate errors.

/** ExTopTabBar.tsx */// modify '../types' to '@react-navigation/material-top-tabs'
import type {MaterialTopTabBarProps} from '@react-navigation/material-top-tabs';
// import ExTabBar
import TabBar from './ExTabBar';

Then install color plugin.

yarn add color @types/color

In ExTabBar.tsx, simply replace ‘.’ with ‘react-native-tab-view/src

’ in the import path to remove errors.

Now, set the ExTopTabBar to Navigator, here is the example in my sample project.

...
import ExTopTabBar from '../components/ExTopTabBar';
...
<MainTab.Navigator
...
tabBar={(props) => <ExTopTabBar {...props} />}>
...

Create a new function getTranslateY, call it in render function with parameter position and layout.height. Then add transform in style for executing animation while gestures happen.

/** ExTabBar.tsx */...
// @ts-ignore
const interpolate = Animated.interpolateNode || Animated.interpolate;
...
private getTranslateY = memoize((position: Animated.Node<number>,
height: number) => {
const inputRange = [0, 1];
const outputRange = [-height, 0];
return interpolate(position, {
inputRange,
outputRange,
extrapolate: Extrapolate.CLAMP,
});
});
...
render(){
...
const translateY = this.getTranslateY(position, layout.height);
...
return (
<Animated.View
...
style={
[
...
{
transform: [{translateY}]
}
]
}
...

Step 3: Add header for ExTabBar to replace the Stack Navigator Header

<Stack.Screen
name="Root"
component={MainTabNavigator}
options={{headerShown: false}}
/>

The custom header is above all its sibling components.

return (<Animated.View
...
transform: [{translateY}],
...>
{/** define header here */}
<View style={{height: 48, top: 36}}>
<Text>Header here</Text>
</View>
{/** header end */}
<Animated.View
...

The effect is shown as follows:

Conclusion

The solution refers to TabBarIndicator source code and simplify the customisation with fewest modification(2 files mentioned). Then you can define the header with any styles you what. Sample project: https://github.com/Pwrecktice/WhatsAppHeader

I’m a software engineer, a runner and a twins’ father~