mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-01-12 22:51:18 +08:00
fix: match native iOS header height in stack
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -80,6 +80,10 @@ type SceneOptionsDefaults = TransitionPreset & {
|
||||
};
|
||||
|
||||
export type Scene = {
|
||||
/**
|
||||
* Route object for the current screen.
|
||||
*/
|
||||
route: Route<string>;
|
||||
/**
|
||||
* Descriptor object for the screen.
|
||||
*/
|
||||
|
||||
@@ -62,6 +62,7 @@ export default React.memo(function Header({
|
||||
progress={progress}
|
||||
insets={insets}
|
||||
layout={layout}
|
||||
modal={isModal}
|
||||
headerBackTitle={
|
||||
options.headerBackTitle !== undefined
|
||||
? options.headerBackTitle
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 ===
|
||||
|
||||
Reference in New Issue
Block a user