Skip to content

Commit

Permalink
fix: calculate values of useHeaderHeight natively (#1802)
Browse files Browse the repository at this point in the history
## Description

Currently, `useHeaderHeight()` hook returns default values, hard-coded
in JS code. It uses `react-native-safe-area-context` under the hood to
calculate top inset of the status bar and returns a header height `56`
(for android) or `64` (for iOS).
This PR introduces new `useAnimatedHeaderHeight` hook that calculates
header height on each layout change.

## Changes

- Added support for calculating header height dynamically on the old /
new architecture for Android and iOS.
- Added event that is being executed on header height change.
- Added new `useAnimatedHeaderHeight` hook for getting header height
that is being calculated dynamically.

## Screenshots / GIFs

### Before


https://github.com/software-mansion/react-native-screens/assets/23281839/1e129ce8-cc62-4333-a6d8-f7283f018441

### After


https://github.com/software-mansion/react-native-screens/assets/23281839/5c11f371-c3f7-4eda-9969-4eb02f9de26a

## Test code and steps to reproduce

See `Test1802` in `TestsExample` or `FabricTestExample`.
There's also `Test1802b` included. It's a reproduction for an issue
#1671 which shows that issue should be fixed after this change.

## Checklist

- [X] Included code example that can be used to test this change
- [X] Updated TS types
- [X] Updated documentation:
- [X]
https://github.com/software-mansion/react-native-screens/blob/main/guides/GUIDE_FOR_LIBRARY_AUTHORS.md
- [X]
https://github.com/software-mansion/react-native-screens/blob/main/native-stack/README.md
- [X]
https://github.com/software-mansion/react-native-screens/blob/main/src/types.tsx
- [x] Ensured that CI passes
  • Loading branch information
tboba authored Sep 18, 2023
1 parent bc899b8 commit 4264f7e
Show file tree
Hide file tree
Showing 28 changed files with 997 additions and 66 deletions.
2 changes: 2 additions & 0 deletions FabricTestExample/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,10 @@ import Test1473 from './src/Test1473';
import Test1476 from './src/Test1476';
import Test1646 from './src/Test1646';
import Test1649 from './src/Test1649';
import Test1671 from './src/Test1671';
import Test1683 from './src/Test1683';
import Test1726 from './src/Test1726';
import Test1802 from './src/Test1802';
import Test1844 from './src/Test1844';
import Test1864 from './src/Test1864';

Expand Down
74 changes: 74 additions & 0 deletions FabricTestExample/src/Test1671.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React from 'react';
import { View } from 'react-native';
import {
useHeaderHeight,
createNativeStackNavigator,
} from 'react-native-screens/native-stack';
import { NavigationContainer } from '@react-navigation/native';

const App = () => {
const backgroundStyle = {
backgroundColor: '#fafffe', // isDarkMode ? Colors.darker : Colors.lighter,
};

return (
<View
style={[
backgroundStyle,
{
flex: 1,
},
]}>
<View
style={{
flex: 1,
height: '100%',
}}>
<Navigation />
</View>
</View>
);
};

const List = () => {
const headerHeight = useHeaderHeight();

return (
<View style={{ flex: 1, backgroundColor: '#00fffa' }}>
<View
style={{
backgroundColor: '#fffa00',
position: 'absolute',
top: headerHeight,
width: 200,
height: 100,
}}
/>
</View>
);
};

const Stack = createNativeStackNavigator();

const Navigation = () => {
return (
<NavigationContainer>
<Stack.Navigator
screenOptions={{
fullScreenSwipeEnabled: true,
stackAnimation: 'fade_from_bottom',
customAnimationOnSwipe: true,
// headerLargeTitle: true,
headerTranslucent: true,
}}>
<Stack.Screen
name="Header"
component={List}
options={{ statusBarStyle: 'dark' }}
/>
</Stack.Navigator>
</NavigationContainer>
);
};

export default App;
174 changes: 174 additions & 0 deletions FabricTestExample/src/Test1802.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import React from 'react';
import { Dimensions, Image, StyleSheet, Text, View } from 'react-native';
import { NavigationContainer, ParamListBase } from '@react-navigation/native';

import {
createNativeStackNavigator,
NativeStackNavigationProp,
} from 'react-native-screens/native-stack';

// Uncomment these lines if you want to test useAnimatedHeaderHeight.
import { Animated } from 'react-native';
import { useAnimatedHeaderHeight } from 'react-native-screens/native-stack';

// Uncomment these lines if you want to test useReanimatedHeaderHeight.
// import Animated from 'react-native-reanimated';
// import { useReanimatedHeaderHeight } from 'react-native-screens/reanimated';

import {
GestureHandlerRootView,
ScrollView,
State,
TapGestureHandler,
} from 'react-native-gesture-handler';
import { ReanimatedScreenProvider } from 'react-native-screens/reanimated';
import { FullWindowOverlay } from 'react-native-screens';

const Stack = createNativeStackNavigator();

function ExampleScreen() {
const headerHeight = useAnimatedHeaderHeight();
// const headerHeight = useReanimatedHeaderHeight();

return (
<FullWindowOverlay>
<Animated.View
style={{
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',

backgroundColor: 'red',
width: '100%',
opacity: 0.5,
height: 60,
zIndex: 1,
transform: [
{
translateY: headerHeight,
},
],
}}>
<Text>I'm a header!</Text>
</Animated.View>
</FullWindowOverlay>
);
}

const enablePerformanceTests = false;

function First({
navigation,
}: {
navigation: NativeStackNavigationProp<ParamListBase>;
}) {
return (
<ScrollView contentInsetAdjustmentBehavior="automatic">
<ExampleScreen />
<Post onPress={() => navigation.navigate('Second')} />
<Post onPress={() => navigation.navigate('Second')} />
<Post onPress={() => navigation.navigate('Second')} />
{
// Generate 1000 posts for performance testing.
enablePerformanceTests &&
Array(1000)
.fill(0)
.map(_ => <Post onPress={() => navigation.navigate('Second')} />)
}
</ScrollView>
);
}

function Second() {
return (
<ScrollView>
<ExampleScreen />
<Text style={styles.subTitle}>
Use swipe back gesture to go back (iOS only)
</Text>
<Post />
</ScrollView>
);
}

export default function App() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<ReanimatedScreenProvider>
<NavigationContainer>
<Stack.Navigator
screenOptions={{
fullScreenSwipeEnabled: true,
stackAnimation: 'fade_from_bottom',
customAnimationOnSwipe: true,
headerLargeTitle: true,
// headerTranslucent: true,
}}>
<Stack.Screen name="First" component={First} />
<Stack.Screen name="Second" component={Second} />
</Stack.Navigator>
</NavigationContainer>
</ReanimatedScreenProvider>
</GestureHandlerRootView>
);
}

// components

function Post({ onPress }: { onPress?: () => void }) {
const [width] = React.useState(Math.round(Dimensions.get('screen').width));

return (
<TapGestureHandler
onHandlerStateChange={e =>
e.nativeEvent.oldState === State.ACTIVE && onPress?.()
}>
<View style={styles.post}>
<Text style={styles.title}>Post</Text>
<ScrollView horizontal>{generatePhotos(4, width, 400)}</ScrollView>
<Text style={styles.caption}>Scroll right for more photos</Text>
</View>
</TapGestureHandler>
);
}

// helpers
function generatePhotos(
amount: number,
width: number,
height: number,
): JSX.Element[] {
const startFrom = Math.floor(Math.random() * 20) + 10;
return Array.from({ length: amount }, (_, index) => {
const uri = `https://picsum.photos/id/${
startFrom + index
}/${width}/${height}`;
return <Image style={{ width, height }} key={uri} source={{ uri }} />;
});
}

const styles = StyleSheet.create({
title: {
fontWeight: 'bold',
fontSize: 32,
marginBottom: 8,
marginLeft: 8,
},
subTitle: {
fontSize: 18,
marginVertical: 16,
textAlign: 'center',
},
caption: {
textAlign: 'center',
marginTop: 4,
},
post: {
borderColor: '#ccc',
borderTopWidth: 1,
borderBottomWidth: 1,
paddingVertical: 10,
marginBottom: 16,
backgroundColor: 'white',
},
});
2 changes: 2 additions & 0 deletions TestsExample/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,11 @@ import Test1509 from './src/Test1509';
import Test1539 from './src/Test1539';
import Test1646 from './src/Test1646';
import Test1649 from './src/Test1649';
import Test1671 from './src/Test1671';
import Test1683 from './src/Test1683';
import Test1726 from './src/Test1726';
import Test1791 from './src/Test1791';
import Test1802 from './src/Test1802';
import Test1844 from './src/Test1844';
import Test1864 from './src/Test1864';

Expand Down
74 changes: 74 additions & 0 deletions TestsExample/src/Test1671.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React from 'react';
import { View } from 'react-native';
import {
useHeaderHeight,
createNativeStackNavigator,
} from 'react-native-screens/native-stack';
import { NavigationContainer } from '@react-navigation/native';

const App = () => {
const backgroundStyle = {
backgroundColor: '#fafffe', // isDarkMode ? Colors.darker : Colors.lighter,
};

return (
<View
style={[
backgroundStyle,
{
flex: 1,
},
]}>
<View
style={{
flex: 1,
height: '100%',
}}>
<Navigation />
</View>
</View>
);
};

const List = () => {
const headerHeight = useHeaderHeight();

return (
<View style={{ flex: 1, backgroundColor: '#00fffa' }}>
<View
style={{
backgroundColor: '#fffa00',
position: 'absolute',
top: headerHeight,
width: 200,
height: 100,
}}
/>
</View>
);
};

const Stack = createNativeStackNavigator();

const Navigation = () => {
return (
<NavigationContainer>
<Stack.Navigator
screenOptions={{
fullScreenSwipeEnabled: true,
stackAnimation: 'fade_from_bottom',
customAnimationOnSwipe: true,
// headerLargeTitle: true,
headerTranslucent: true,
}}>
<Stack.Screen
name="Header"
component={List}
options={{ statusBarStyle: 'dark' }}
/>
</Stack.Navigator>
</NavigationContainer>
);
};

export default App;
Loading

0 comments on commit 4264f7e

Please sign in to comment.