/* eslint-disable camelcase */import*asReactfrom'react';import{Animated,Platform,StyleSheet,ViewProps,ViewStyle,}from'react-native';//@ts-ignore Getting private component// eslint-disable-next-line import/no-named-as-default, import/default, import/no-named-as-default-member, import/namespaceimportAppContainerfrom'react-native/Libraries/ReactNative/AppContainer';importwarnOncefrom'warn-once';import{StackPresentationTypes,ScreensRefsHolder}from'../../types';importScreenStackfrom'../../components/ScreenStack';importScreenContentWrapperfrom'../../components/ScreenContentWrapper';import{ScreenContext}from'../../components/Screen';import{ParamListBase,StackActions,StackNavigationState,useTheme,Route,NavigationState,PartialState,}from'@react-navigation/native';import{useSafeAreaFrame,useSafeAreaInsets,}from'react-native-safe-area-context';import{NativeStackDescriptorMap,NativeStackNavigationHelpers,NativeStackNavigationOptions,}from'../types';importHeaderConfigfrom'./HeaderConfig';importSafeAreaProviderCompatfrom'../utils/SafeAreaProviderCompat';importgetDefaultHeaderHeightfrom'../utils/getDefaultHeaderHeight';importgetStatusBarHeightfrom'../utils/getStatusBarHeight';importHeaderHeightContextfrom'../utils/HeaderHeightContext';importAnimatedHeaderHeightContextfrom'../utils/AnimatedHeaderHeightContext';importFooterComponentfrom'./FooterComponent';constisAndroid=Platform.OS==='android';letContainer=ScreenContentWrapper;if(__DEV__){constDebugContainer=(props:ViewProps&{stackPresentation:StackPresentationTypes},)=>{const{ stackPresentation, ...rest}=props;if(Platform.OS==='ios'&&stackPresentation!=='push'&&stackPresentation!=='formSheet'){return(<AppContainer><ScreenContentWrapper{...rest}/></AppContainer>);}return<ScreenContentWrapper{...rest}/>;};//@ts-ignore Wrong propsContainer=DebugContainer;}constMaybeNestedStack=({ options, route, stackPresentation, children, internalScreenStyle,}:{options:NativeStackNavigationOptions;route:Route<string>;stackPresentation:StackPresentationTypes;children:React.ReactNode;internalScreenStyle?:Pick<ViewStyle,'backgroundColor'>;})=>{const{ colors}=useTheme();const{ headerShown=true, contentStyle}=options;constScreen=React.useContext(ScreenContext);constisHeaderInModal=isAndroid ?false :stackPresentation!=='push'&&headerShown===true;constheaderShownPreviousRef=React.useRef(headerShown);React.useEffect(()=>{warnOnce(!isAndroid&&stackPresentation!=='push'&&headerShownPreviousRef.current!==headerShown,`Dynamically changing 'headerShown' in modals will result in remounting the screen and losing all local state. See options for the screen '${route.name}'.`,);headerShownPreviousRef.current=headerShown;},[headerShown,stackPresentation,route.name]);constcontent=(<Containerstyle={[stackPresentation==='formSheet' ?Platform.OS==='ios' ?styles.absoluteFillNoBottom :null :styles.container,stackPresentation!=='transparentModal'&&stackPresentation!=='containedTransparentModal'&&{backgroundColor:colors.background,},contentStyle,]}//@ts-ignore Wrong props passed to ViewstackPresentation={stackPresentation}// This view must *not* be flattened.// See https://github.com/software-mansion/react-native-screens/pull/1825// for detailed explanation.collapsable={false}>{children}</Container>);constdimensions=useSafeAreaFrame();consttopInset=useSafeAreaInsets().top;constisStatusBarTranslucent=options.statusBarTranslucent??false;conststatusBarHeight=getStatusBarHeight(topInset,dimensions,isStatusBarTranslucent,);consthasLargeHeader=options.headerLargeTitle??false;constheaderHeight=getDefaultHeaderHeight(dimensions,statusBarHeight,stackPresentation,hasLargeHeader,);if(isHeaderInModal){return(<ScreenStackstyle={styles.container}><ScreenenabledisNativeStackhasLargeHeader={hasLargeHeader}style={[StyleSheet.absoluteFill,internalScreenStyle]}><HeaderHeightContext.Providervalue={headerHeight}><HeaderConfig{...options}route={route}/>{content}</HeaderHeightContext.Provider></Screen></ScreenStack>);}returncontent;};typeNavigationRoute<ParamListextendsParamListBase,RouteNameextendskeyofParamList,>=Route<Extract<RouteName,string>,ParamList[RouteName]>&{state?:NavigationState|PartialState<NavigationState>;};constRouteView=({ descriptors, route, index, navigation, stateKey, screensRefs,}:{descriptors:NativeStackDescriptorMap;route:NavigationRoute<ParamListBase,string>;index:number;navigation:NativeStackNavigationHelpers;stateKey:string;screensRefs:React.MutableRefObject<ScreensRefsHolder>;})=>{const{ options,render:renderScene}=descriptors[route.key];const{ fullScreenSwipeShadowEnabled=false, gestureEnabled, headerShown, hideKeyboardOnSwipe, homeIndicatorHidden, sheetLargestUndimmedDetentIndex='none', sheetGrabberVisible=false, sheetCornerRadius=-1.0, sheetElevation=24, sheetExpandsWhenScrolledToEdge=true, sheetInitialDetentIndex=0, nativeBackButtonDismissalEnabled=false, navigationBarColor, navigationBarTranslucent, navigationBarHidden, replaceAnimation='pop', screenOrientation, statusBarAnimation, statusBarColor, statusBarHidden, statusBarStyle, statusBarTranslucent, swipeDirection='horizontal', transitionDuration, freezeOnBlur, unstable_sheetFooter=null, contentStyle,}=options;let{ sheetAllowedDetents=[1.0], customAnimationOnSwipe, fullScreenSwipeEnabled, gestureResponseDistance, stackAnimation, stackPresentation='push',}=options;// We take backgroundColor from contentStyle and apply it on Screen.// This allows to workaround one issue with truncated// content with formSheet presentation.letinternalScreenStyle;if(stackPresentation==='formSheet'&&contentStyle){constflattenContentStyles=StyleSheet.flatten(contentStyle);internalScreenStyle={backgroundColor:flattenContentStyles?.backgroundColor,};}if(sheetAllowedDetents==='fitToContents'){sheetAllowedDetents=[-1];}if(swipeDirection==='vertical'){// for `vertical` direction to work, we need to set `fullScreenSwipeEnabled` to `true`// so the screen can be dismissed from any point on screen.// `customAnimationOnSwipe` needs to be set to `true` so the `stackAnimation` set by user can be used,// otherwise `simple_push` will be used.// Also, the default animation for this direction seems to be `slide_from_bottom`.if(fullScreenSwipeEnabled===undefined){fullScreenSwipeEnabled=true;}if(customAnimationOnSwipe===undefined){customAnimationOnSwipe=true;}if(stackAnimation===undefined){stackAnimation='slide_from_bottom';}}if(index===0){// first screen should always be treated as `push`, it resolves problems with no header animation// for navigator with first screen as `modal` and the next as `push`stackPresentation='push';}constdimensions=useSafeAreaFrame();consttopInset=useSafeAreaInsets().top;constisStatusBarTranslucent=options.statusBarTranslucent??false;conststatusBarHeight=getStatusBarHeight(topInset,dimensions,isStatusBarTranslucent,);consthasLargeHeader=options.headerLargeTitle??false;constdefaultHeaderHeight=getDefaultHeaderHeight(dimensions,statusBarHeight,stackPresentation,hasLargeHeader,);constparentHeaderHeight=React.useContext(HeaderHeightContext);constisHeaderInPush=isAndroid ?headerShown :stackPresentation==='push'&&headerShown!==false;conststaticHeaderHeight=isHeaderInPush!==false ?defaultHeaderHeight :parentHeaderHeight??0;// We need to ensure the first retrieved header height will be cached and set in animatedHeaderHeight.// We're caching the header height here, as on iOS native side events are not always coming to the JS on first notify.// TODO: Check why first event is not being received once it is cached on the native side.constcachedAnimatedHeaderHeight=React.useRef(defaultHeaderHeight);constanimatedHeaderHeight=React.useRef(newAnimated.Value(staticHeaderHeight,{useNativeDriver:true,}),).current;constScreen=React.useContext(ScreenContext);const{ dark}=useTheme();constscreenRef=React.useRef(null);React.useEffect(()=>{screensRefs.current[route.key]=screenRef;return()=>{// eslint-disable-next-line @typescript-eslint/no-dynamic-deletedeletescreensRefs.current[route.key];};});return(<Screenkey={route.key}ref={screenRef}enabledisNativeStackhasLargeHeader={hasLargeHeader}style={[StyleSheet.absoluteFill,internalScreenStyle]}sheetAllowedDetents={sheetAllowedDetents}sheetLargestUndimmedDetentIndex={sheetLargestUndimmedDetentIndex}sheetGrabberVisible={sheetGrabberVisible}sheetInitialDetentIndex={sheetInitialDetentIndex}sheetCornerRadius={sheetCornerRadius}sheetElevation={sheetElevation}sheetExpandsWhenScrolledToEdge={sheetExpandsWhenScrolledToEdge}customAnimationOnSwipe={customAnimationOnSwipe}freezeOnBlur={freezeOnBlur}fullScreenSwipeEnabled={fullScreenSwipeEnabled}fullScreenSwipeShadowEnabled={fullScreenSwipeShadowEnabled}hideKeyboardOnSwipe={hideKeyboardOnSwipe}homeIndicatorHidden={homeIndicatorHidden}gestureEnabled={isAndroid ?false :gestureEnabled}gestureResponseDistance={gestureResponseDistance}nativeBackButtonDismissalEnabled={nativeBackButtonDismissalEnabled}navigationBarColor={navigationBarColor}navigationBarTranslucent={navigationBarTranslucent}navigationBarHidden={navigationBarHidden}replaceAnimation={replaceAnimation}screenOrientation={screenOrientation}stackAnimation={stackAnimation}stackPresentation={stackPresentation}statusBarAnimation={statusBarAnimation}statusBarColor={statusBarColor}statusBarHidden={statusBarHidden}statusBarStyle={statusBarStyle??(dark ?'light' :'dark')}statusBarTranslucent={statusBarTranslucent}swipeDirection={swipeDirection}transitionDuration={transitionDuration}onHeaderBackButtonClicked={()=>{navigation.dispatch({ ...StackActions.pop(),source:route.key,target:stateKey,});}}onWillAppear={()=>{navigation.emit({type:'transitionStart',data:{closing:false},target:route.key,});}}onWillDisappear={()=>{navigation.emit({type:'transitionStart',data:{closing:true},target:route.key,});}}onAppear={()=>{navigation.emit({type:'appear',target:route.key,});navigation.emit({type:'transitionEnd',data:{closing:false},target:route.key,});}}onDisappear={()=>{navigation.emit({type:'transitionEnd',data:{closing:true},target:route.key,});}}onHeaderHeightChange={e=>{constheaderHeight=e.nativeEvent.headerHeight;if(cachedAnimatedHeaderHeight.current!==headerHeight){// Currently, we're setting value by Animated#setValue, because we want to cache animated value.// Also, in React Native 0.72 there was a bug on Fabric causing a large delay between the screen transition,// which should not occur.// TODO: Check if it's possible to replace animated#setValue to Animated#event.animatedHeaderHeight.setValue(headerHeight);cachedAnimatedHeaderHeight.current=headerHeight;}}}onDismissed={e=>{navigation.emit({type:'dismiss',target:route.key,});constdismissCount=e.nativeEvent.dismissCount>0 ?e.nativeEvent.dismissCount :1;navigation.dispatch({ ...StackActions.pop(dismissCount),source:route.key,target:stateKey,});}}onSheetDetentChanged={e=>{navigation.emit({type:'sheetDetentChange',target:route.key,data:{index:e.nativeEvent.index,isStable:e.nativeEvent.isStable,},});}}onGestureCancel={()=>{navigation.emit({type:'gestureCancel',target:route.key,});}}><AnimatedHeaderHeightContext.Providervalue={animatedHeaderHeight}><HeaderHeightContext.Providervalue={staticHeaderHeight}><MaybeNestedStackoptions={options}route={route}stackPresentation={stackPresentation}internalScreenStyle={internalScreenStyle}>{renderScene()}</MaybeNestedStack>{/* HeaderConfig must not be first child of a Screen. See https://github.com/software-mansion/react-native-screens/pull/1825 for detailed explanation */}<HeaderConfig{...options}route={route}headerShown={isHeaderInPush}/>{stackPresentation==='formSheet'&&unstable_sheetFooter&&(<FooterComponent>{unstable_sheetFooter()}</FooterComponent>)}</HeaderHeightContext.Provider></AnimatedHeaderHeightContext.Provider></Screen>);};typeProps={state:StackNavigationState<ParamListBase>;navigation:NativeStackNavigationHelpers;descriptors:NativeStackDescriptorMap;};functionNativeStackViewInner({ state, navigation, descriptors,}:Props):JSX.Element{const{ key, routes}=state;constcurrentRouteKey=routes[state.index].key;const{ goBackGesture, transitionAnimation, screenEdgeGesture}=descriptors[currentRouteKey].options;constscreensRefs=React.useRef<ScreensRefsHolder>({});return(<ScreenStackstyle={styles.container}goBackGesture={goBackGesture}transitionAnimation={transitionAnimation}screenEdgeGesture={screenEdgeGesture??false}screensRefs={screensRefs}currentScreenId={currentRouteKey}>{routes.map((route,index)=>(<RouteViewkey={route.key}descriptors={descriptors}route={route}index={index}navigation={navigation}stateKey={key}screensRefs={screensRefs}/>))}</ScreenStack>);}exportdefaultfunctionNativeStackView(props:Props){return(<SafeAreaProviderCompat><NativeStackViewInner{...props}/></SafeAreaProviderCompat>);}conststyles=StyleSheet.create({container:{flex:1,},absoluteFill:{position:'absolute',top:0,left:0,right:0,bottom:0,},absoluteFillNoBottom:{position:'absolute',top:0,left:0,right:0,},});
Uh oh!
There was an error while loading.Please reload this page.
Continuation of#12204 and#11943. Changes from original version:
In general, the idea suggested by@satya164 was that we could move more logic into react-native-screens and avoid leaking it to@react-navigation.
Original description:
Motivation
This PR intents to add implementation of custom screen transitions, recently added in react-native-screens. You can find more about this featurehere.
Test plan
Go to the code of the native stack in example (NativeStack.tsx) and wrap whole navigator with (import from
react-native-screens/gesture-handler
). After that, usegestureType
prop on the desired screen.Ready to paste code 🍝
Presentation
Screen.Recording.2024-04-16.at.18.18.51.mov