mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-01-12 22:51:18 +08:00
feat: add custom header option to native-stack
This commit is contained in:
@@ -4,6 +4,7 @@ import type {
|
||||
NavigationHelpers,
|
||||
NavigationProp,
|
||||
ParamListBase,
|
||||
Route,
|
||||
RouteProp,
|
||||
StackActionHelpers,
|
||||
StackNavigationState,
|
||||
@@ -60,11 +61,39 @@ export type NativeStackNavigationHelpers = NavigationHelpers<
|
||||
// We want it to be an empty object because navigator does not have any additional props
|
||||
export type NativeStackNavigationConfig = {};
|
||||
|
||||
export type NativeStackHeaderProps = {
|
||||
/**
|
||||
* Options for the back button.
|
||||
*/
|
||||
back?: {
|
||||
/**
|
||||
* Title of the previous screen.
|
||||
*/
|
||||
title: string;
|
||||
};
|
||||
/**
|
||||
* Options for the current screen.
|
||||
*/
|
||||
options: NativeStackNavigationOptions;
|
||||
/**
|
||||
* Route object for the current screen.
|
||||
*/
|
||||
route: Route<string>;
|
||||
/**
|
||||
* Navigation prop for the header.
|
||||
*/
|
||||
navigation: NativeStackNavigationProp<ParamListBase>;
|
||||
};
|
||||
|
||||
export type NativeStackNavigationOptions = {
|
||||
/**
|
||||
* String that can be displayed in the header as a fallback for `headerTitle`.
|
||||
*/
|
||||
title?: string;
|
||||
/**
|
||||
* Function that given `HeaderProps` returns a React Element to display as a header.
|
||||
*/
|
||||
header?: (props: NativeStackHeaderProps) => React.ReactNode;
|
||||
/**
|
||||
* Whether the back button is visible in the header.
|
||||
* You can use it to show a back button alongside `headerLeft` if you have specified it.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
getDefaultHeaderHeight,
|
||||
getHeaderTitle,
|
||||
HeaderHeightContext,
|
||||
HeaderShownContext,
|
||||
SafeAreaProviderCompat,
|
||||
@@ -47,11 +48,11 @@ const MaybeNestedStack = ({
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const { colors } = useTheme();
|
||||
const { headerShown = true, contentStyle } = options;
|
||||
const { header, headerShown = true, contentStyle } = options;
|
||||
|
||||
const isHeaderInModal = isAndroid
|
||||
? false
|
||||
: presentation !== 'card' && headerShown === true;
|
||||
: presentation !== 'card' && headerShown === true && header === undefined;
|
||||
|
||||
const headerShownPreviousRef = React.useRef(headerShown);
|
||||
|
||||
@@ -120,6 +121,7 @@ const MaybeNestedStack = ({
|
||||
type SceneViewProps = {
|
||||
index: number;
|
||||
descriptor: NativeStackDescriptor;
|
||||
previousDescriptor?: NativeStackDescriptor;
|
||||
onWillDisappear: () => void;
|
||||
onAppear: () => void;
|
||||
onDisappear: () => void;
|
||||
@@ -128,15 +130,17 @@ type SceneViewProps = {
|
||||
|
||||
const SceneView = ({
|
||||
descriptor,
|
||||
previousDescriptor,
|
||||
index,
|
||||
onWillDisappear,
|
||||
onAppear,
|
||||
onDisappear,
|
||||
onDismissed,
|
||||
}: SceneViewProps) => {
|
||||
const { route, options, render } = descriptor;
|
||||
const { route, navigation, options, render } = descriptor;
|
||||
const {
|
||||
gestureEnabled,
|
||||
header,
|
||||
headerShown,
|
||||
animationTypeForReplace = 'pop',
|
||||
animation,
|
||||
@@ -199,11 +203,28 @@ const SceneView = ({
|
||||
isHeaderInPush !== false ? headerHeight : parentHeaderHeight ?? 0
|
||||
}
|
||||
>
|
||||
<HeaderConfig
|
||||
{...options}
|
||||
route={route}
|
||||
headerShown={isHeaderInPush}
|
||||
/>
|
||||
{header !== undefined && headerShown !== false ? (
|
||||
// TODO: expose custom header height
|
||||
header({
|
||||
back: previousDescriptor
|
||||
? {
|
||||
title: getHeaderTitle(
|
||||
previousDescriptor.options,
|
||||
previousDescriptor.route.name
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
options,
|
||||
route,
|
||||
navigation,
|
||||
})
|
||||
) : (
|
||||
<HeaderConfig
|
||||
{...options}
|
||||
route={route}
|
||||
headerShown={isHeaderInPush}
|
||||
/>
|
||||
)}
|
||||
<MaybeNestedStack
|
||||
options={options}
|
||||
route={route}
|
||||
@@ -244,43 +265,52 @@ function NativeStackViewInner({ state, navigation, descriptors }: Props) {
|
||||
|
||||
return (
|
||||
<ScreenStack style={styles.container}>
|
||||
{state.routes.map((route, index) => (
|
||||
<SceneView
|
||||
key={route.key}
|
||||
index={index}
|
||||
descriptor={descriptors[route.key]}
|
||||
onWillDisappear={() => {
|
||||
navigation.emit({
|
||||
type: 'transitionStart',
|
||||
data: { closing: true },
|
||||
target: route.key,
|
||||
});
|
||||
}}
|
||||
onAppear={() => {
|
||||
navigation.emit({
|
||||
type: 'transitionEnd',
|
||||
data: { closing: false },
|
||||
target: route.key,
|
||||
});
|
||||
}}
|
||||
onDisappear={() => {
|
||||
navigation.emit({
|
||||
type: 'transitionEnd',
|
||||
data: { closing: true },
|
||||
target: route.key,
|
||||
});
|
||||
}}
|
||||
onDismissed={() => {
|
||||
navigation.dispatch({
|
||||
...StackActions.pop(),
|
||||
source: route.key,
|
||||
target: state.key,
|
||||
});
|
||||
{state.routes.map((route, index) => {
|
||||
const descriptor = descriptors[route.key];
|
||||
const previousKey = state.routes[index - 1]?.key;
|
||||
const previousDescriptor = previousKey
|
||||
? descriptors[previousKey]
|
||||
: undefined;
|
||||
|
||||
setNextDismissedKey(route.key);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
return (
|
||||
<SceneView
|
||||
key={route.key}
|
||||
index={index}
|
||||
descriptor={descriptor}
|
||||
previousDescriptor={previousDescriptor}
|
||||
onWillDisappear={() => {
|
||||
navigation.emit({
|
||||
type: 'transitionStart',
|
||||
data: { closing: true },
|
||||
target: route.key,
|
||||
});
|
||||
}}
|
||||
onAppear={() => {
|
||||
navigation.emit({
|
||||
type: 'transitionEnd',
|
||||
data: { closing: false },
|
||||
target: route.key,
|
||||
});
|
||||
}}
|
||||
onDisappear={() => {
|
||||
navigation.emit({
|
||||
type: 'transitionEnd',
|
||||
data: { closing: true },
|
||||
target: route.key,
|
||||
});
|
||||
}}
|
||||
onDismissed={() => {
|
||||
navigation.dispatch({
|
||||
...StackActions.pop(),
|
||||
source: route.key,
|
||||
target: state.key,
|
||||
});
|
||||
|
||||
setNextDismissedKey(route.key);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ScreenStack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,9 +32,14 @@ export default function NativeStackView({ state, descriptors }: Props) {
|
||||
{state.routes.map((route, i) => {
|
||||
const isFocused = state.index === i;
|
||||
const canGoBack = i !== 0;
|
||||
const previousKey = state.routes[i - 1]?.key;
|
||||
const previousDescriptor = previousKey
|
||||
? descriptors[previousKey]
|
||||
: undefined;
|
||||
const { options, navigation, render } = descriptors[route.key];
|
||||
|
||||
const {
|
||||
header,
|
||||
headerShown,
|
||||
headerTintColor,
|
||||
headerBackImageSource,
|
||||
@@ -56,57 +61,76 @@ export default function NativeStackView({ state, descriptors }: Props) {
|
||||
navigation={navigation}
|
||||
headerShown={headerShown}
|
||||
header={
|
||||
<Header
|
||||
title={getHeaderTitle(options, route.name)}
|
||||
headerTintColor={headerTintColor}
|
||||
headerLeft={
|
||||
typeof headerLeft === 'function'
|
||||
? ({ tintColor }) => headerLeft({ tintColor })
|
||||
: headerLeft === undefined && canGoBack
|
||||
? ({ tintColor }) => (
|
||||
<HeaderBackButton
|
||||
tintColor={tintColor}
|
||||
backImage={
|
||||
headerBackImageSource !== undefined
|
||||
? () => (
|
||||
<Image
|
||||
source={headerBackImageSource}
|
||||
style={[styles.backImage, { tintColor }]}
|
||||
/>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
onPress={navigation.goBack}
|
||||
canGoBack={canGoBack}
|
||||
/>
|
||||
)
|
||||
: headerLeft
|
||||
}
|
||||
headerRight={
|
||||
typeof headerRight === 'function'
|
||||
? ({ tintColor }) => headerRight({ tintColor })
|
||||
: headerRight
|
||||
}
|
||||
headerTitle={
|
||||
typeof headerTitle === 'function'
|
||||
? ({ children, tintColor }) =>
|
||||
headerTitle({ children, tintColor })
|
||||
: headerTitle
|
||||
}
|
||||
headerTitleStyle={headerTitleStyle}
|
||||
headerStyle={[
|
||||
headerTranslucent
|
||||
header !== undefined ? (
|
||||
header({
|
||||
back: previousDescriptor
|
||||
? {
|
||||
position: 'absolute',
|
||||
backgroundColor: 'transparent',
|
||||
title: getHeaderTitle(
|
||||
previousDescriptor.options,
|
||||
previousDescriptor.route.name
|
||||
),
|
||||
}
|
||||
: null,
|
||||
headerStyle,
|
||||
headerShadowVisible === false
|
||||
? { shadowOpacity: 0, borderBottomWidth: 0 }
|
||||
: null,
|
||||
]}
|
||||
/>
|
||||
: undefined,
|
||||
options,
|
||||
route,
|
||||
navigation,
|
||||
})
|
||||
) : (
|
||||
<Header
|
||||
title={getHeaderTitle(options, route.name)}
|
||||
headerTintColor={headerTintColor}
|
||||
headerLeft={
|
||||
typeof headerLeft === 'function'
|
||||
? ({ tintColor }) => headerLeft({ tintColor })
|
||||
: headerLeft === undefined && canGoBack
|
||||
? ({ tintColor }) => (
|
||||
<HeaderBackButton
|
||||
tintColor={tintColor}
|
||||
backImage={
|
||||
headerBackImageSource !== undefined
|
||||
? () => (
|
||||
<Image
|
||||
source={headerBackImageSource}
|
||||
style={[
|
||||
styles.backImage,
|
||||
{ tintColor },
|
||||
]}
|
||||
/>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
onPress={navigation.goBack}
|
||||
canGoBack={canGoBack}
|
||||
/>
|
||||
)
|
||||
: headerLeft
|
||||
}
|
||||
headerRight={
|
||||
typeof headerRight === 'function'
|
||||
? ({ tintColor }) => headerRight({ tintColor })
|
||||
: headerRight
|
||||
}
|
||||
headerTitle={
|
||||
typeof headerTitle === 'function'
|
||||
? ({ children, tintColor }) =>
|
||||
headerTitle({ children, tintColor })
|
||||
: headerTitle
|
||||
}
|
||||
headerTitleStyle={headerTitleStyle}
|
||||
headerStyle={[
|
||||
headerTranslucent
|
||||
? {
|
||||
position: 'absolute',
|
||||
backgroundColor: 'transparent',
|
||||
}
|
||||
: null,
|
||||
headerStyle,
|
||||
headerShadowVisible === false
|
||||
? { shadowOpacity: 0, borderBottomWidth: 0 }
|
||||
: null,
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
style={[
|
||||
StyleSheet.absoluteFill,
|
||||
|
||||
Reference in New Issue
Block a user