Skip to content

Commit 78afbff

Browse files
authored
fix: relatively position float Header if !headerTransparent (facebook#8285)
## Motivation Right now `headerMode: float` renders an absolutely-positioned header. To offset the content appropriately, it then measures the height of the header and compensates with a margin. This approach unfortunately doesn't work well for animations. Before | After :-------------------------:|:-------------------------: <img src="http://ashoat.com/jerky_absolute.gif" width="300" /> | <img src="http://ashoat.com/smooth_relative.gif" width="300" /> ## Approach When rendering the header absolutely we want to render it above (after, in sibling order) the content. But when rendering it relatively we want to render it first (before, in sibling order). The margin compensation code is no longer necessary so I removed it. ## Test plan I used the `StackHeaderCustomization` example to make sure transitions between `headerTransparent` and `!headerTransparent` looked good. I added a custom (taller) header to test if height transitions looked good, and toggled `headerShown` to make sure that transitioned well too. Would be open to any other suggestions of things to test!
1 parent 762cc44 commit 78afbff

File tree

3 files changed

+72
-22
lines changed

3 files changed

+72
-22
lines changed

example/src/Screens/StackHeaderCustomization.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import * as React from 'react';
2-
import { View, StyleSheet, ScrollView, Alert, Platform } from 'react-native';
2+
import {
3+
View,
4+
StyleSheet,
5+
ScrollView,
6+
Alert,
7+
Platform,
8+
Text,
9+
} from 'react-native';
310
import { Button, Appbar } from 'react-native-paper';
411
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
512
import { RouteProp, ParamListBase } from '@react-navigation/native';
@@ -8,6 +15,8 @@ import {
815
StackNavigationProp,
916
HeaderBackground,
1017
useHeaderHeight,
18+
Header,
19+
StackHeaderProps,
1120
} from '@react-navigation/stack';
1221
import BlurView from '../Shared/BlurView';
1322
import Article from '../Shared/Article';
@@ -91,6 +100,20 @@ type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> & {
91100
navigation: StackNavigationProp<ParamListBase>;
92101
};
93102

103+
function CustomHeader(props: StackHeaderProps) {
104+
const { navigation } = props;
105+
return (
106+
<>
107+
<Header {...props} />
108+
<View style={{ opacity: navigation.isFocused() ? 1 : 0 }}>
109+
<Text style={{ textAlign: 'center', backgroundColor: 'pink' }}>
110+
Why hello there, pardner!
111+
</Text>
112+
</View>
113+
</>
114+
);
115+
}
116+
94117
export default function SimpleStackScreen({ navigation, ...rest }: Props) {
95118
navigation.setOptions({
96119
headerShown: false,
@@ -103,6 +126,7 @@ export default function SimpleStackScreen({ navigation, ...rest }: Props) {
103126
component={ArticleScreen}
104127
options={({ route }) => ({
105128
title: `Article by ${route.params?.author}`,
129+
header: CustomHeader,
106130
headerTintColor: '#fff',
107131
headerStyle: { backgroundColor: '#ff005d' },
108132
headerBackTitleVisible: false,

packages/stack/src/views/Stack/CardContainer.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ type Props = TransitionPreset & {
5555
headerMode: StackHeaderMode;
5656
headerShown?: boolean;
5757
headerTransparent?: boolean;
58+
isFloatHeaderAbsolute: boolean;
5859
headerHeight: number;
5960
onHeaderHeightChange: (props: {
6061
route: Route<string>;
@@ -85,6 +86,7 @@ function CardContainer({
8586
headerShown,
8687
headerStyleInterpolator,
8788
headerTransparent,
89+
isFloatHeaderAbsolute,
8890
headerHeight,
8991
onHeaderHeightChange,
9092
index,
@@ -188,7 +190,7 @@ function CardContainer({
188190
pointerEvents={active ? 'box-none' : pointerEvents}
189191
pageOverflowEnabled={headerMode === 'screen' && mode === 'card'}
190192
containerStyle={
191-
headerMode === 'float' && !headerTransparent && headerShown !== false
193+
isFloatHeaderAbsolute && !headerTransparent && headerShown !== false
192194
? { marginTop: headerHeight }
193195
: null
194196
}

packages/stack/src/views/Stack/CardStack.tsx

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,21 @@ export default class CardStack extends React.Component<Props, State> {
335335
return state.routes[state.index];
336336
};
337337

338+
private getSomeFloatHeaderNeedsAbsolutePositioning = () => {
339+
if (this.props.headerMode !== 'float') {
340+
return false;
341+
}
342+
return this.state.scenes.slice(-2).some((scene) => {
343+
const { descriptor } = scene;
344+
const options = descriptor ? descriptor.options : {};
345+
const { headerTransparent, headerShown } = options;
346+
if (headerTransparent || headerShown === false) {
347+
return true;
348+
}
349+
return false;
350+
});
351+
};
352+
338353
render() {
339354
const {
340355
mode,
@@ -362,6 +377,7 @@ export default class CardStack extends React.Component<Props, State> {
362377
const focusedRoute = state.routes[state.index];
363378
const focusedDescriptor = descriptors[focusedRoute.key];
364379
const focusedOptions = focusedDescriptor ? focusedDescriptor.options : {};
380+
const isFloatHeaderAbsolute = this.getSomeFloatHeaderNeedsAbsolutePositioning();
365381

366382
let defaultTransitionPreset =
367383
mode === 'modal' ? ModalTransition : DefaultTransition;
@@ -384,8 +400,34 @@ export default class CardStack extends React.Component<Props, State> {
384400
// For modals, usually we want the screen underneath to be visible, so also disable it there
385401
const isScreensEnabled = Platform.OS !== 'ios' && mode !== 'modal';
386402

403+
let floatingHeader;
404+
if (headerMode === 'float') {
405+
const renderedHeader = renderHeader({
406+
mode: 'float',
407+
layout,
408+
insets: { top, right, bottom, left },
409+
scenes,
410+
getPreviousRoute,
411+
getFocusedRoute: this.getFocusedRoute,
412+
onContentHeightChange: this.handleHeaderLayout,
413+
gestureDirection:
414+
focusedOptions.gestureDirection !== undefined
415+
? focusedOptions.gestureDirection
416+
: defaultTransitionPreset.gestureDirection,
417+
styleInterpolator:
418+
focusedOptions.headerStyleInterpolator !== undefined
419+
? focusedOptions.headerStyleInterpolator
420+
: defaultTransitionPreset.headerStyleInterpolator,
421+
style: isFloatHeaderAbsolute ? styles.floating : undefined,
422+
});
423+
floatingHeader = (
424+
<React.Fragment key="floatingHeader">{renderedHeader}</React.Fragment>
425+
);
426+
}
427+
387428
return (
388429
<React.Fragment>
430+
{isFloatHeaderAbsolute ? null : floatingHeader}
389431
<MaybeScreenContainer
390432
enabled={isScreensEnabled}
391433
style={styles.container}
@@ -524,6 +566,7 @@ export default class CardStack extends React.Component<Props, State> {
524566
headerMode={headerMode}
525567
headerShown={headerShown}
526568
headerTransparent={headerTransparent}
569+
isFloatHeaderAbsolute={isFloatHeaderAbsolute}
527570
renderHeader={renderHeader}
528571
renderScene={renderScene}
529572
onOpenRoute={onOpenRoute}
@@ -538,26 +581,7 @@ export default class CardStack extends React.Component<Props, State> {
538581
);
539582
})}
540583
</MaybeScreenContainer>
541-
{headerMode === 'float'
542-
? renderHeader({
543-
mode: 'float',
544-
layout,
545-
insets: { top, right, bottom, left },
546-
scenes,
547-
getPreviousRoute,
548-
getFocusedRoute: this.getFocusedRoute,
549-
onContentHeightChange: this.handleHeaderLayout,
550-
gestureDirection:
551-
focusedOptions.gestureDirection !== undefined
552-
? focusedOptions.gestureDirection
553-
: defaultTransitionPreset.gestureDirection,
554-
styleInterpolator:
555-
focusedOptions.headerStyleInterpolator !== undefined
556-
? focusedOptions.headerStyleInterpolator
557-
: defaultTransitionPreset.headerStyleInterpolator,
558-
style: styles.floating,
559-
})
560-
: null}
584+
{isFloatHeaderAbsolute ? floatingHeader : null}
561585
</React.Fragment>
562586
);
563587
}

0 commit comments

Comments
 (0)