From 78afbffe976b14bb60666a2b1230127db0dc24f6 Mon Sep 17 00:00:00 2001 From: Ashoat Tevosyan Date: Fri, 5 Jun 2020 18:12:00 -0400 Subject: [PATCH] fix: relatively position float Header if !headerTransparent (#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 :-------------------------:|:-------------------------: | ## 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! --- .../src/Screens/StackHeaderCustomization.tsx | 26 +++++++- .../stack/src/views/Stack/CardContainer.tsx | 4 +- packages/stack/src/views/Stack/CardStack.tsx | 64 +++++++++++++------ 3 files changed, 72 insertions(+), 22 deletions(-) diff --git a/example/src/Screens/StackHeaderCustomization.tsx b/example/src/Screens/StackHeaderCustomization.tsx index a78a2b24..75dafc65 100644 --- a/example/src/Screens/StackHeaderCustomization.tsx +++ b/example/src/Screens/StackHeaderCustomization.tsx @@ -1,5 +1,12 @@ import * as React from 'react'; -import { View, StyleSheet, ScrollView, Alert, Platform } from 'react-native'; +import { + View, + StyleSheet, + ScrollView, + Alert, + Platform, + Text, +} from 'react-native'; import { Button, Appbar } from 'react-native-paper'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import { RouteProp, ParamListBase } from '@react-navigation/native'; @@ -8,6 +15,8 @@ import { StackNavigationProp, HeaderBackground, useHeaderHeight, + Header, + StackHeaderProps, } from '@react-navigation/stack'; import BlurView from '../Shared/BlurView'; import Article from '../Shared/Article'; @@ -91,6 +100,20 @@ type Props = Partial> & { navigation: StackNavigationProp; }; +function CustomHeader(props: StackHeaderProps) { + const { navigation } = props; + return ( + <> +
+ + + Why hello there, pardner! + + + + ); +} + export default function SimpleStackScreen({ navigation, ...rest }: Props) { navigation.setOptions({ headerShown: false, @@ -103,6 +126,7 @@ export default function SimpleStackScreen({ navigation, ...rest }: Props) { component={ArticleScreen} options={({ route }) => ({ title: `Article by ${route.params?.author}`, + header: CustomHeader, headerTintColor: '#fff', headerStyle: { backgroundColor: '#ff005d' }, headerBackTitleVisible: false, diff --git a/packages/stack/src/views/Stack/CardContainer.tsx b/packages/stack/src/views/Stack/CardContainer.tsx index 26651644..8c7955c9 100644 --- a/packages/stack/src/views/Stack/CardContainer.tsx +++ b/packages/stack/src/views/Stack/CardContainer.tsx @@ -55,6 +55,7 @@ type Props = TransitionPreset & { headerMode: StackHeaderMode; headerShown?: boolean; headerTransparent?: boolean; + isFloatHeaderAbsolute: boolean; headerHeight: number; onHeaderHeightChange: (props: { route: Route; @@ -85,6 +86,7 @@ function CardContainer({ headerShown, headerStyleInterpolator, headerTransparent, + isFloatHeaderAbsolute, headerHeight, onHeaderHeightChange, index, @@ -188,7 +190,7 @@ function CardContainer({ pointerEvents={active ? 'box-none' : pointerEvents} pageOverflowEnabled={headerMode === 'screen' && mode === 'card'} containerStyle={ - headerMode === 'float' && !headerTransparent && headerShown !== false + isFloatHeaderAbsolute && !headerTransparent && headerShown !== false ? { marginTop: headerHeight } : null } diff --git a/packages/stack/src/views/Stack/CardStack.tsx b/packages/stack/src/views/Stack/CardStack.tsx index 6ad3b217..8e44a276 100755 --- a/packages/stack/src/views/Stack/CardStack.tsx +++ b/packages/stack/src/views/Stack/CardStack.tsx @@ -335,6 +335,21 @@ export default class CardStack extends React.Component { return state.routes[state.index]; }; + private getSomeFloatHeaderNeedsAbsolutePositioning = () => { + if (this.props.headerMode !== 'float') { + return false; + } + return this.state.scenes.slice(-2).some((scene) => { + const { descriptor } = scene; + const options = descriptor ? descriptor.options : {}; + const { headerTransparent, headerShown } = options; + if (headerTransparent || headerShown === false) { + return true; + } + return false; + }); + }; + render() { const { mode, @@ -362,6 +377,7 @@ export default class CardStack extends React.Component { const focusedRoute = state.routes[state.index]; const focusedDescriptor = descriptors[focusedRoute.key]; const focusedOptions = focusedDescriptor ? focusedDescriptor.options : {}; + const isFloatHeaderAbsolute = this.getSomeFloatHeaderNeedsAbsolutePositioning(); let defaultTransitionPreset = mode === 'modal' ? ModalTransition : DefaultTransition; @@ -384,8 +400,34 @@ export default class CardStack extends React.Component { // For modals, usually we want the screen underneath to be visible, so also disable it there const isScreensEnabled = Platform.OS !== 'ios' && mode !== 'modal'; + let floatingHeader; + if (headerMode === 'float') { + const renderedHeader = renderHeader({ + mode: 'float', + layout, + insets: { top, right, bottom, left }, + scenes, + getPreviousRoute, + getFocusedRoute: this.getFocusedRoute, + onContentHeightChange: this.handleHeaderLayout, + gestureDirection: + focusedOptions.gestureDirection !== undefined + ? focusedOptions.gestureDirection + : defaultTransitionPreset.gestureDirection, + styleInterpolator: + focusedOptions.headerStyleInterpolator !== undefined + ? focusedOptions.headerStyleInterpolator + : defaultTransitionPreset.headerStyleInterpolator, + style: isFloatHeaderAbsolute ? styles.floating : undefined, + }); + floatingHeader = ( + {renderedHeader} + ); + } + return ( + {isFloatHeaderAbsolute ? null : floatingHeader} { headerMode={headerMode} headerShown={headerShown} headerTransparent={headerTransparent} + isFloatHeaderAbsolute={isFloatHeaderAbsolute} renderHeader={renderHeader} renderScene={renderScene} onOpenRoute={onOpenRoute} @@ -538,26 +581,7 @@ export default class CardStack extends React.Component { ); })} - {headerMode === 'float' - ? renderHeader({ - mode: 'float', - layout, - insets: { top, right, bottom, left }, - scenes, - getPreviousRoute, - getFocusedRoute: this.getFocusedRoute, - onContentHeightChange: this.handleHeaderLayout, - gestureDirection: - focusedOptions.gestureDirection !== undefined - ? focusedOptions.gestureDirection - : defaultTransitionPreset.gestureDirection, - styleInterpolator: - focusedOptions.headerStyleInterpolator !== undefined - ? focusedOptions.headerStyleInterpolator - : defaultTransitionPreset.headerStyleInterpolator, - style: styles.floating, - }) - : null} + {isFloatHeaderAbsolute ? floatingHeader : null} ); }