fix: match native iOS header height in stack

This commit is contained in:
Satyajit Sahoo
2021-07-28 13:50:07 +02:00
parent 9506ad1f00
commit 51b636d726
8 changed files with 209 additions and 167 deletions

View File

@@ -12,6 +12,10 @@ import HeaderShownContext from './HeaderShownContext';
import HeaderTitle from './HeaderTitle';
type Props = HeaderOptions & {
/**
* Whether the header is in a modal
*/
modal?: boolean;
/**
* Layout of the screen.
*/
@@ -46,6 +50,7 @@ export default function Header(props: Props) {
const {
layout = frame,
modal = false,
title,
headerTitle: customTitle,
headerTitleAlign = Platform.select({
@@ -69,7 +74,11 @@ export default function Header(props: Props) {
headerStatusBarHeight = isParentHeaderShown ? 0 : insets.top,
} = props;
const defaultHeight = getDefaultHeaderHeight(layout, headerStatusBarHeight);
const defaultHeight = getDefaultHeaderHeight(
layout,
modal,
headerStatusBarHeight
);
const {
height = defaultHeight,

View File

@@ -4,17 +4,30 @@ import type { Layout } from '../types';
export default function getDefaultHeaderHeight(
layout: Layout,
modal: boolean,
statusBarHeight: number
): number {
const isLandscape = layout.width > layout.height;
let headerHeight;
const isLandscape = layout.width > layout.height;
if (Platform.OS === 'ios') {
if (isLandscape && !Platform.isPad) {
headerHeight = 32;
if (Platform.isPad) {
if (modal) {
headerHeight = 56;
} else {
headerHeight = 50;
}
} else {
headerHeight = 44;
if (isLandscape) {
headerHeight = 32;
} else {
if (modal) {
headerHeight = 56;
} else {
headerHeight = 44;
}
}
}
} else if (Platform.OS === 'android') {
headerHeight = 56;

View File

@@ -19,6 +19,7 @@ import HeaderShownContext from './Header/HeaderShownContext';
type Props = {
focused: boolean;
modal?: boolean;
navigation: NavigationProp<ParamListBase>;
route: RouteProp<ParamListBase>;
header: React.ReactNode;
@@ -37,6 +38,7 @@ export default function Screen(props: Props) {
const {
focused,
modal = false,
header,
headerShown = true,
headerStatusBarHeight = isParentHeaderShown ? 0 : insets.top,
@@ -47,7 +49,7 @@ export default function Screen(props: Props) {
} = props;
const [headerHeight, setHeaderHeight] = React.useState(() =>
getDefaultHeaderHeight(dimensions, headerStatusBarHeight)
getDefaultHeaderHeight(dimensions, modal, headerStatusBarHeight)
);
return (

View File

@@ -80,6 +80,10 @@ type SceneOptionsDefaults = TransitionPreset & {
};
export type Scene = {
/**
* Route object for the current screen.
*/
route: Route<string>;
/**
* Descriptor object for the screen.
*/

View File

@@ -62,6 +62,7 @@ export default React.memo(function Header({
progress={progress}
insets={insets}
layout={layout}
modal={isModal}
headerBackTitle={
options.headerBackTitle !== undefined
? options.headerBackTitle

View File

@@ -26,6 +26,7 @@ type Props = StackHeaderOptions & {
layout: Layout;
title: string;
insets: EdgeInsets;
modal: boolean;
onGoBack?: () => void;
progress: SceneProgress;
styleInterpolator: StackHeaderStyleInterpolator;
@@ -99,6 +100,7 @@ export default function HeaderSegment(props: Props) {
progress,
insets,
layout,
modal,
onGoBack,
headerTitle: title,
headerLeft: left,
@@ -120,7 +122,11 @@ export default function HeaderSegment(props: Props) {
...rest
} = props;
const defaultHeight = getDefaultHeaderHeight(layout, headerStatusBarHeight);
const defaultHeight = getDefaultHeaderHeight(
layout,
modal,
headerStatusBarHeight
);
const { height = defaultHeight } = StyleSheet.flatten(
customHeaderStyle || {}
@@ -172,6 +178,7 @@ export default function HeaderSegment(props: Props) {
return (
<Header
modal={modal}
layout={layout}
headerTitle={headerTitle}
headerLeft={headerLeft}

View File

@@ -21,7 +21,6 @@ export default function ModalStatusBarManager({
const { dark: darkTheme } = useTheme();
const [overlapping, setOverlapping] = React.useState(true);
const enabled = layout.width && layout.height > layout.width;
const scale = 1 - 20 / layout.width;
const offset = (insets.top - 34) * scale;
@@ -31,10 +30,6 @@ export default function ModalStatusBarManager({
)?.translateY;
React.useEffect(() => {
if (!enabled) {
return;
}
const listener = ({ value }: { value: number }) => {
setOverlapping(value < offset);
};
@@ -42,11 +37,7 @@ export default function ModalStatusBarManager({
const sub = translateY?.addListener(listener);
return () => translateY?.removeListener(sub);
}, [enabled, offset, translateY]);
if (!enabled) {
return null;
}
}, [offset, translateY]);
const darkContent = dark ?? !darkTheme;

View File

@@ -87,28 +87,56 @@ const STATE_ON_TOP = 2;
const FALLBACK_DESCRIPTOR = Object.freeze({ options: {} });
const getInterpolationIndex = (scenes: Scene[], index: number) => {
const { cardStyleInterpolator } = scenes[index].descriptor.options;
// Start from current card and count backwards the number of cards with same interpolation
let interpolationIndex = 0;
for (let i = index - 1; i >= 0; i--) {
const cardStyleInterpolatorCurrent =
scenes[i]?.descriptor.options.cardStyleInterpolator;
if (cardStyleInterpolatorCurrent !== cardStyleInterpolator) {
break;
}
interpolationIndex++;
}
return interpolationIndex;
};
const getHeaderHeights = (
routes: Route<string>[],
scenes: Scene[],
insets: EdgeInsets,
isParentHeaderShown: boolean,
descriptors: StackDescriptorMap,
layout: Layout,
previous: Record<string, number>
) => {
return routes.reduce<Record<string, number>>((acc, curr) => {
const { options = {} } = descriptors[curr.key] || {};
const style: any = StyleSheet.flatten(options.headerStyle || {});
return scenes.reduce<Record<string, number>>((acc, curr, index) => {
const {
headerStatusBarHeight = isParentHeaderShown ? 0 : insets.top,
cardStyleInterpolator,
headerStyle,
} = curr.descriptor.options;
const style = StyleSheet.flatten(headerStyle || {});
const height =
typeof style.height === 'number' ? style.height : previous[curr.key];
typeof style.height === 'number'
? style.height
: previous[curr.route.key];
const { headerStatusBarHeight = isParentHeaderShown ? 0 : insets.top } =
options;
const interpolationIndex = getInterpolationIndex(scenes, index);
const isModalPresentation =
cardStyleInterpolator === forModalPresentationIOS;
const isModal = isModalPresentation && interpolationIndex !== 0;
acc[curr.key] =
acc[curr.route.key] =
typeof height === 'number'
? height
: getDefaultHeaderHeight(layout, headerStatusBarHeight);
: getDefaultHeaderHeight(layout, isModal, headerStatusBarHeight);
return acc;
}, {});
@@ -184,152 +212,152 @@ export default class CardStack extends React.Component<Props, State> {
return acc;
}, {});
return {
routes: props.routes,
scenes: props.routes.map((route, index, self) => {
const previousRoute = self[index - 1];
const nextRoute = self[index + 1];
const scenes = props.routes.map((route, index, self) => {
const previousRoute = self[index - 1];
const nextRoute = self[index + 1];
const oldScene = state.scenes[index];
const oldScene = state.scenes[index];
const currentGesture = gestures[route.key];
const previousGesture = previousRoute
? gestures[previousRoute.key]
: undefined;
const nextGesture = nextRoute ? gestures[nextRoute.key] : undefined;
const currentGesture = gestures[route.key];
const previousGesture = previousRoute
? gestures[previousRoute.key]
: undefined;
const nextGesture = nextRoute ? gestures[nextRoute.key] : undefined;
const descriptor =
props.descriptors[route.key] ||
state.descriptors[route.key] ||
(oldScene ? oldScene.descriptor : FALLBACK_DESCRIPTOR);
const descriptor =
props.descriptors[route.key] ||
state.descriptors[route.key] ||
(oldScene ? oldScene.descriptor : FALLBACK_DESCRIPTOR);
const nextDescriptor =
props.descriptors[nextRoute?.key] ||
state.descriptors[nextRoute?.key];
const nextDescriptor =
props.descriptors[nextRoute?.key] || state.descriptors[nextRoute?.key];
const previousDescriptor =
props.descriptors[previousRoute?.key] ||
state.descriptors[previousRoute?.key];
const previousDescriptor =
props.descriptors[previousRoute?.key] ||
state.descriptors[previousRoute?.key];
// When a screen is not the last, it should use next screen's transition config
// Many transitions also animate the previous screen, so using 2 different transitions doesn't look right
// For example combining a slide and a modal transition would look wrong otherwise
// With this approach, combining different transition styles in the same navigator mostly looks right
// This will still be broken when 2 transitions have different idle state (e.g. modal presentation),
// but majority of the transitions look alright
const optionsForTransitionConfig =
index !== self.length - 1 &&
nextDescriptor &&
nextDescriptor.options.presentation !== 'transparentModal'
? nextDescriptor.options
: descriptor.options;
// When a screen is not the last, it should use next screen's transition config
// Many transitions also animate the previous screen, so using 2 different transitions doesn't look right
// For example combining a slide and a modal transition would look wrong otherwise
// With this approach, combining different transition styles in the same navigator mostly looks right
// This will still be broken when 2 transitions have different idle state (e.g. modal presentation),
// but majority of the transitions look alright
const optionsForTransitionConfig =
index !== self.length - 1 &&
nextDescriptor &&
nextDescriptor.options.presentation !== 'transparentModal'
? nextDescriptor.options
: descriptor.options;
let defaultTransitionPreset =
optionsForTransitionConfig.presentation === 'modal'
? ModalTransition
: optionsForTransitionConfig.presentation === 'transparentModal'
? ModalFadeTransition
: DefaultTransition;
let defaultTransitionPreset =
optionsForTransitionConfig.presentation === 'modal'
? ModalTransition
: optionsForTransitionConfig.presentation === 'transparentModal'
? ModalFadeTransition
: DefaultTransition;
const {
animationEnabled = Platform.OS !== 'web' &&
Platform.OS !== 'windows' &&
Platform.OS !== 'macos',
gestureEnabled = Platform.OS === 'ios' && animationEnabled,
gestureDirection = defaultTransitionPreset.gestureDirection,
transitionSpec = defaultTransitionPreset.transitionSpec,
cardStyleInterpolator = animationEnabled === false
? forNoAnimationCard
: defaultTransitionPreset.cardStyleInterpolator,
headerStyleInterpolator = defaultTransitionPreset.headerStyleInterpolator,
cardOverlayEnabled = (Platform.OS !== 'ios' &&
optionsForTransitionConfig.presentation !== 'transparentModal') ||
cardStyleInterpolator === forModalPresentationIOS,
} = optionsForTransitionConfig;
const {
animationEnabled = Platform.OS !== 'web' &&
Platform.OS !== 'windows' &&
Platform.OS !== 'macos',
gestureEnabled = Platform.OS === 'ios' && animationEnabled,
gestureDirection = defaultTransitionPreset.gestureDirection,
transitionSpec = defaultTransitionPreset.transitionSpec,
cardStyleInterpolator = animationEnabled === false
? forNoAnimationCard
: defaultTransitionPreset.cardStyleInterpolator,
headerStyleInterpolator = defaultTransitionPreset.headerStyleInterpolator,
cardOverlayEnabled = (Platform.OS !== 'ios' &&
optionsForTransitionConfig.presentation !== 'transparentModal') ||
cardStyleInterpolator === forModalPresentationIOS,
} = optionsForTransitionConfig;
const headerMode: StackHeaderMode =
descriptor.options.headerMode ??
(!(
optionsForTransitionConfig.presentation === 'modal' ||
optionsForTransitionConfig.presentation === 'transparentModal' ||
nextDescriptor?.options.presentation === 'modal' ||
nextDescriptor?.options.presentation === 'transparentModal' ||
cardStyleInterpolator === forModalPresentationIOS
) &&
Platform.OS === 'ios' &&
descriptor.options.header === undefined
? 'float'
: 'screen');
const headerMode: StackHeaderMode =
descriptor.options.headerMode ??
(!(
optionsForTransitionConfig.presentation === 'modal' ||
optionsForTransitionConfig.presentation === 'transparentModal' ||
nextDescriptor?.options.presentation === 'modal' ||
nextDescriptor?.options.presentation === 'transparentModal' ||
cardStyleInterpolator === forModalPresentationIOS
) &&
Platform.OS === 'ios' &&
descriptor.options.header === undefined
? 'float'
: 'screen');
const scene = {
route,
descriptor: {
...descriptor,
options: {
...descriptor.options,
animationEnabled,
cardOverlayEnabled,
cardStyleInterpolator,
gestureDirection,
gestureEnabled,
headerStyleInterpolator,
transitionSpec,
headerMode,
},
const scene = {
route,
descriptor: {
...descriptor,
options: {
...descriptor.options,
animationEnabled,
cardOverlayEnabled,
cardStyleInterpolator,
gestureDirection,
gestureEnabled,
headerStyleInterpolator,
transitionSpec,
headerMode,
},
progress: {
current: getProgressFromGesture(
currentGesture,
state.layout,
descriptor
),
next:
nextGesture &&
nextDescriptor.options.presentation !== 'transparentModal'
? getProgressFromGesture(
nextGesture,
state.layout,
nextDescriptor
)
: undefined,
previous: previousGesture
},
progress: {
current: getProgressFromGesture(
currentGesture,
state.layout,
descriptor
),
next:
nextGesture &&
nextDescriptor.options.presentation !== 'transparentModal'
? getProgressFromGesture(
previousGesture,
nextGesture,
state.layout,
previousDescriptor
nextDescriptor
)
: undefined,
},
__memo: [
state.layout,
descriptor,
nextDescriptor,
previousDescriptor,
currentGesture,
nextGesture,
previousGesture,
],
};
previous: previousGesture
? getProgressFromGesture(
previousGesture,
state.layout,
previousDescriptor
)
: undefined,
},
__memo: [
state.layout,
descriptor,
nextDescriptor,
previousDescriptor,
currentGesture,
nextGesture,
previousGesture,
],
};
if (
oldScene &&
scene.__memo.every((it, i) => {
// @ts-expect-error: we haven't added __memo to the annotation to prevent usage elsewhere
return oldScene.__memo[i] === it;
})
) {
return oldScene;
}
if (
oldScene &&
scene.__memo.every((it, i) => {
// @ts-expect-error: we haven't added __memo to the annotation to prevent usage elsewhere
return oldScene.__memo[i] === it;
})
) {
return oldScene;
}
return scene;
}),
return scene;
});
return {
routes: props.routes,
scenes,
gestures,
descriptors: props.descriptors,
headerHeights: getHeaderHeights(
props.routes,
scenes,
props.insets,
props.isParentHeaderShown,
state.descriptors,
state.layout,
state.headerHeights
),
@@ -367,10 +395,9 @@ export default class CardStack extends React.Component<Props, State> {
return {
layout,
headerHeights: getHeaderHeights(
props.routes,
state.scenes,
props.insets,
props.isParentHeaderShown,
state.descriptors,
layout,
state.headerHeights
),
@@ -549,7 +576,6 @@ export default class CardStack extends React.Component<Props, State> {
}
const {
cardStyleInterpolator,
headerShown = true,
headerTransparent,
headerStyle,
@@ -578,18 +604,7 @@ export default class CardStack extends React.Component<Props, State> {
}
// Start from current card and count backwards the number of cards with same interpolation
let interpolationIndex = 0;
for (let i = index - 1; i >= 0; i--) {
const cardStyleInterpolatorCurrent =
scenes[i]?.descriptor.options.cardStyleInterpolator;
if (cardStyleInterpolatorCurrent !== cardStyleInterpolator) {
break;
}
interpolationIndex++;
}
const interpolationIndex = getInterpolationIndex(scenes, index);
const isNextScreenTransparent =
scenes[index + 1]?.descriptor.options.presentation ===