diff --git a/src/native-stack/utils/getStatusBarHeight.tsx b/src/native-stack/utils/getStatusBarHeight.tsx new file mode 100644 index 0000000000..e54ec5c2d9 --- /dev/null +++ b/src/native-stack/utils/getStatusBarHeight.tsx @@ -0,0 +1,22 @@ +import { Rect } from 'react-native-safe-area-context'; +import { Platform } from 'react-native'; + +export default function getStatusBarHeight( + topInset: number, + dimensions: Rect, + isStatusBarTranslucent: boolean +) { + if (Platform.OS === 'ios') { + // It looks like some iOS devices don't have strictly set status bar height to 44. + // Thus, if the top inset is higher than 50, then the device should have a dynamic island. + // On models with Dynamic Island the status bar height is smaller than the safe area top inset by 5 pixels. + // See https://developer.apple.com/forums/thread/662466 for more details about status bar height. + const hasDynamicIsland = topInset > 50; + return hasDynamicIsland ? topInset - 5 : topInset; + } else if (Platform.OS === 'android') { + // On Android we should also rely on frame's y-axis position, as topInset is 0 on visible status bar. + return isStatusBarTranslucent ? topInset : dimensions.y; + } + + return topInset; +} diff --git a/src/native-stack/views/NativeStackView.tsx b/src/native-stack/views/NativeStackView.tsx index eaad757dd4..43768aae92 100644 --- a/src/native-stack/views/NativeStackView.tsx +++ b/src/native-stack/views/NativeStackView.tsx @@ -19,7 +19,6 @@ import { PartialState, } from '@react-navigation/native'; import { - Rect, useSafeAreaFrame, useSafeAreaInsets, } from 'react-native-safe-area-context'; @@ -31,6 +30,7 @@ import { import HeaderConfig from './HeaderConfig'; import SafeAreaProviderCompat from '../utils/SafeAreaProviderCompat'; import getDefaultHeaderHeight from '../utils/getDefaultHeaderHeight'; +import getStatusBarHeight from '../utils/getStatusBarHeight'; import HeaderHeightContext from '../utils/HeaderHeightContext'; import AnimatedHeaderHeightContext from '../utils/AnimatedHeaderHeightContext'; @@ -118,19 +118,23 @@ const MaybeNestedStack = ({ isStatusBarTranslucent ); - const isLargeHeader = options.headerLargeTitle ?? false; + const hasLargeHeader = options.headerLargeTitle ?? false; const headerHeight = getDefaultHeaderHeight( dimensions, statusBarHeight, stackPresentation, - isLargeHeader + hasLargeHeader ); if (isHeaderInModal) { return ( - + {content} @@ -228,13 +232,13 @@ const RouteView = ({ isStatusBarTranslucent ); - const isLargeHeader = options.headerLargeTitle ?? false; + const hasLargeHeader = options.headerLargeTitle ?? false; const defaultHeaderHeight = getDefaultHeaderHeight( dimensions, statusBarHeight, stackPresentation, - isLargeHeader + hasLargeHeader ); const parentHeaderHeight = React.useContext(HeaderHeightContext); @@ -264,6 +268,7 @@ const RouteView = ({ key={route.key} enabled isNativeStack + hasLargeHeader={hasLargeHeader} style={StyleSheet.absoluteFill} sheetAllowedDetents={sheetAllowedDetents} sheetLargestUndimmedDetent={sheetLargestUndimmedDetent} @@ -422,26 +427,6 @@ export default function NativeStackView(props: Props) { ); } -function getStatusBarHeight( - topInset: number, - dimensions: Rect, - isStatusBarTranslucent: boolean -) { - if (Platform.OS === 'ios') { - // It looks like some iOS devices don't have strictly set status bar height to 44. - // Thus, if the top inset is higher than 50, then the device should have a dynamic island. - // On models with Dynamic Island the status bar height is smaller than the safe area top inset by 5 pixels. - // See https://developer.apple.com/forums/thread/662466 for more details about status bar height. - const hasDynamicIsland = topInset > 50; - return hasDynamicIsland ? topInset - 5 : topInset; - } else if (Platform.OS === 'android') { - // On Android we should also rely on frame's y-axis position, as topInset is 0 on visible status bar. - return isStatusBarTranslucent ? topInset : dimensions.y; - } - - return topInset; -} - const styles = StyleSheet.create({ container: { flex: 1, diff --git a/src/reanimated/ReanimatedNativeStackScreen.tsx b/src/reanimated/ReanimatedNativeStackScreen.tsx index 8232a6c59b..c5773a4229 100644 --- a/src/reanimated/ReanimatedNativeStackScreen.tsx +++ b/src/reanimated/ReanimatedNativeStackScreen.tsx @@ -15,6 +15,7 @@ import { useSafeAreaInsets, } from 'react-native-safe-area-context'; import getDefaultHeaderHeight from '../native-stack/utils/getDefaultHeaderHeight'; +import getStatusBarHeight from '../native-stack/utils/getStatusBarHeight'; import ReanimatedHeaderHeightContext from './ReanimatedHeaderHeightContext'; const AnimatedScreen = Animated.createAnimatedComponent( @@ -31,23 +32,24 @@ const ReanimatedNativeStackScreen = React.forwardRef< ScreenProps >((props, ref) => { const { children, ...rest } = props; - const { stackPresentation = 'push' } = rest; + const { stackPresentation = 'push', hasLargeHeader } = rest; const dimensions = useSafeAreaFrame(); const topInset = useSafeAreaInsets().top; - let statusBarHeight = topInset; - const hasDynamicIsland = Platform.OS === 'ios' && topInset === 59; - if (hasDynamicIsland) { - // On models with Dynamic Island the status bar height is smaller than the safe area top inset. - statusBarHeight = 54; - } + const isStatusBarTranslucent = rest.statusBarTranslucent ?? false; + const statusBarHeight = getStatusBarHeight( + topInset, + dimensions, + isStatusBarTranslucent + ); // Default header height, normally used in `useHeaderHeight` hook. // Here, it is used for returning a default value for shared value. const defaultHeaderHeight = getDefaultHeaderHeight( dimensions, statusBarHeight, - stackPresentation + stackPresentation, + hasLargeHeader ); const cachedHeaderHeight = React.useRef(defaultHeaderHeight); diff --git a/src/reanimated/useReanimatedHeaderHeight.tsx b/src/reanimated/useReanimatedHeaderHeight.tsx index a012613c5b..c2b6a8015f 100644 --- a/src/reanimated/useReanimatedHeaderHeight.tsx +++ b/src/reanimated/useReanimatedHeaderHeight.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import ReanimatedHeaderHeightContext from './ReanimatedHeaderHeightContext'; -export default function useReanimatedTransitionProgress() { +export default function useReanimatedHeaderHeight() { const height = React.useContext(ReanimatedHeaderHeightContext); if (height === undefined) { diff --git a/src/types.tsx b/src/types.tsx index 8026eaa303..379d8b35d1 100644 --- a/src/types.tsx +++ b/src/types.tsx @@ -111,6 +111,10 @@ export interface ScreenProps extends ViewProps { * Internal boolean used to not attach events used only by native-stack. It prevents non native-stack navigators from sending transition progress from their Screen components. */ isNativeStack?: boolean; + /** + * Internal boolean used to detect if current header has large title on iOS. + */ + hasLargeHeader?: boolean; /** * Whether inactive screens should be suspended from re-rendering. Defaults to `false`. * When `enableFreeze()` is run at the top of the application defaults to `true`.