Skip to content

Commit

Permalink
fix: Allow Reanimated Screen to check large header (#1915)
Browse files Browse the repository at this point in the history
## Description

It looks that on Reanimated Screen we didn't check if on iOS there's
large header enabled, which led to the wrong initial values, when user
was using *at least* ReanimatedScreenProvider.

This feature fixes that by adding an ability to get screen descriptor by
Reanimated Screen, which contains an information if user has enabled
`headerLargeTitle` prop.

## Changes

- Changed the `useReanimatedTransitionProgress` leftover inside the
`useReanimatedHeaderHeight` file
- Created new hook `useScreenInfo` that provides an information about
the route view which is being used by ReanimatedNativeStackScreen (this
hook is not meant to be public [at least for now] - that's why I didn't
exported it)
- Moved the implementation of getStatusBarHeight to the separate file

## Screenshots / GIFs

### Before

<img width="995" alt="image"
src="https://github.com/software-mansion/react-native-screens/assets/23281839/244782fb-8ef0-433e-b84f-7a7159f800d9">

### After

<img width="996" alt="image"
src="https://github.com/software-mansion/react-native-screens/assets/23281839/2114196c-1528-409a-8c04-0e6d1d9cc7c7">

## Test code and steps to reproduce

Add to `Test42` ReanimatedScreenProvider and enable `headerLargeTitle`
prop for the `Home` screen.

## Checklist

- [X] Included code example that can be used to test this change
- [X] Updated TS types
- [ ] Ensured that CI passes
  • Loading branch information
tboba authored Oct 11, 2023
1 parent 2468905 commit 20fccf1
Show file tree
Hide file tree
Showing 5 changed files with 48 additions and 35 deletions.
22 changes: 22 additions & 0 deletions src/native-stack/utils/getStatusBarHeight.tsx
Original file line number Diff line number Diff line change
@@ -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;
}
37 changes: 11 additions & 26 deletions src/native-stack/views/NativeStackView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
PartialState,
} from '@react-navigation/native';
import {
Rect,
useSafeAreaFrame,
useSafeAreaInsets,
} from 'react-native-safe-area-context';
Expand All @@ -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';

Expand Down Expand Up @@ -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 (
<ScreenStack style={styles.container}>
<Screen enabled isNativeStack style={StyleSheet.absoluteFill}>
<Screen
enabled
isNativeStack
hasLargeHeader={hasLargeHeader}
style={StyleSheet.absoluteFill}>
<HeaderHeightContext.Provider value={headerHeight}>
<HeaderConfig {...options} route={route} />
{content}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -264,6 +268,7 @@ const RouteView = ({
key={route.key}
enabled
isNativeStack
hasLargeHeader={hasLargeHeader}
style={StyleSheet.absoluteFill}
sheetAllowedDetents={sheetAllowedDetents}
sheetLargestUndimmedDetent={sheetLargestUndimmedDetent}
Expand Down Expand Up @@ -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,
Expand Down
18 changes: 10 additions & 8 deletions src/reanimated/ReanimatedNativeStackScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/reanimated/useReanimatedHeaderHeight.tsx
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions src/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down

0 comments on commit 20fccf1

Please sign in to comment.