You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
504 lines
15 KiB
504 lines
15 KiB
import React, { PropsWithChildren, ReactNode } from 'react';
|
|
import {
|
|
Animated,
|
|
Image,
|
|
ImageProps,
|
|
Platform,
|
|
requireNativeComponent,
|
|
StyleProp,
|
|
StyleSheet,
|
|
UIManager,
|
|
View,
|
|
ViewProps,
|
|
ViewStyle,
|
|
} from 'react-native';
|
|
import { Freeze } from 'react-freeze';
|
|
import { version } from 'react-native/package.json';
|
|
|
|
import TransitionProgressContext from './TransitionProgressContext';
|
|
import useTransitionProgress from './useTransitionProgress';
|
|
import {
|
|
StackPresentationTypes,
|
|
StackAnimationTypes,
|
|
BlurEffectTypes,
|
|
ScreenReplaceTypes,
|
|
ScreenOrientationTypes,
|
|
HeaderSubviewTypes,
|
|
ScreenProps,
|
|
ScreenContainerProps,
|
|
ScreenStackProps,
|
|
ScreenStackHeaderConfigProps,
|
|
SearchBarProps,
|
|
} from './types';
|
|
import {
|
|
isSearchBarAvailableForCurrentPlatform,
|
|
executeNativeBackPress,
|
|
} from './utils';
|
|
|
|
// web implementation is taken from `index.tsx`
|
|
const isPlatformSupported =
|
|
Platform.OS === 'ios' ||
|
|
Platform.OS === 'android' ||
|
|
Platform.OS === 'windows';
|
|
|
|
let ENABLE_SCREENS = isPlatformSupported;
|
|
|
|
function enableScreens(shouldEnableScreens = true): void {
|
|
ENABLE_SCREENS = isPlatformSupported && shouldEnableScreens;
|
|
if (ENABLE_SCREENS && !UIManager.getViewManagerConfig('RNSScreen')) {
|
|
console.error(
|
|
`Screen native module hasn't been linked. Please check the react-native-screens README for more details`
|
|
);
|
|
}
|
|
}
|
|
|
|
let ENABLE_FREEZE = false;
|
|
|
|
function enableFreeze(shouldEnableReactFreeze = true): void {
|
|
const minor = parseInt(version.split('.')[1]); // eg. takes 66 from '0.66.0'
|
|
|
|
// react-freeze requires react-native >=0.64, react-native from main is 0.0.0
|
|
if (!(minor === 0 || minor >= 64) && shouldEnableReactFreeze) {
|
|
console.warn(
|
|
'react-freeze library requires at least react-native 0.64. Please upgrade your react-native version in order to use this feature.'
|
|
);
|
|
}
|
|
|
|
ENABLE_FREEZE = shouldEnableReactFreeze;
|
|
}
|
|
|
|
// const that tells if the library should use new implementation, will be undefined for older versions
|
|
const shouldUseActivityState = true;
|
|
|
|
function screensEnabled(): boolean {
|
|
return ENABLE_SCREENS;
|
|
}
|
|
|
|
// We initialize these lazily so that importing the module doesn't throw error when not linked
|
|
// This is necessary coz libraries such as React Navigation import the library where it may not be enabled
|
|
let NativeScreenValue: React.ComponentType<ScreenProps>;
|
|
let NativeScreenContainerValue: React.ComponentType<ScreenContainerProps>;
|
|
let NativeScreenNavigationContainerValue: React.ComponentType<ScreenContainerProps>;
|
|
let NativeScreenStack: React.ComponentType<ScreenStackProps>;
|
|
let NativeScreenStackHeaderConfig: React.ComponentType<ScreenStackHeaderConfigProps>;
|
|
let NativeScreenStackHeaderSubview: React.ComponentType<
|
|
React.PropsWithChildren<ViewProps & { type?: HeaderSubviewTypes }>
|
|
>;
|
|
let AnimatedNativeScreen: React.ComponentType<ScreenProps>;
|
|
let NativeSearchBar: React.ComponentType<SearchBarProps>;
|
|
let NativeFullWindowOverlay: React.ComponentType<
|
|
PropsWithChildren<{
|
|
style: StyleProp<ViewStyle>;
|
|
}>
|
|
>;
|
|
|
|
const ScreensNativeModules = {
|
|
get NativeScreen() {
|
|
NativeScreenValue =
|
|
NativeScreenValue || requireNativeComponent('RNSScreen');
|
|
return NativeScreenValue;
|
|
},
|
|
|
|
get NativeScreenContainer() {
|
|
NativeScreenContainerValue =
|
|
NativeScreenContainerValue ||
|
|
requireNativeComponent('RNSScreenContainer');
|
|
return NativeScreenContainerValue;
|
|
},
|
|
|
|
get NativeScreenNavigationContainer() {
|
|
NativeScreenNavigationContainerValue =
|
|
NativeScreenNavigationContainerValue ||
|
|
(Platform.OS === 'ios'
|
|
? requireNativeComponent('RNSScreenNavigationContainer')
|
|
: this.NativeScreenContainer);
|
|
return NativeScreenNavigationContainerValue;
|
|
},
|
|
|
|
get NativeScreenStack() {
|
|
NativeScreenStack =
|
|
NativeScreenStack || requireNativeComponent('RNSScreenStack');
|
|
return NativeScreenStack;
|
|
},
|
|
|
|
get NativeScreenStackHeaderConfig() {
|
|
NativeScreenStackHeaderConfig =
|
|
NativeScreenStackHeaderConfig ||
|
|
requireNativeComponent('RNSScreenStackHeaderConfig');
|
|
return NativeScreenStackHeaderConfig;
|
|
},
|
|
|
|
get NativeScreenStackHeaderSubview() {
|
|
NativeScreenStackHeaderSubview =
|
|
NativeScreenStackHeaderSubview ||
|
|
requireNativeComponent('RNSScreenStackHeaderSubview');
|
|
return NativeScreenStackHeaderSubview;
|
|
},
|
|
|
|
get NativeSearchBar() {
|
|
NativeSearchBar = NativeSearchBar || requireNativeComponent('RNSSearchBar');
|
|
return NativeSearchBar;
|
|
},
|
|
|
|
get NativeFullWindowOverlay() {
|
|
NativeFullWindowOverlay =
|
|
NativeFullWindowOverlay || requireNativeComponent('RNSFullWindowOverlay');
|
|
return NativeFullWindowOverlay;
|
|
},
|
|
};
|
|
|
|
interface FreezeWrapperProps {
|
|
freeze: boolean;
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
// This component allows one more render before freezing the screen.
|
|
// Allows activityState to reach the native side and useIsFocused to work correctly.
|
|
function DelayedFreeze({ freeze, children }: FreezeWrapperProps) {
|
|
// flag used for determining whether freeze should be enabled
|
|
const [freezeState, setFreezeState] = React.useState(false);
|
|
|
|
if (freeze !== freezeState) {
|
|
// setImmediate is executed at the end of the JS execution block.
|
|
// Used here for changing the state right after the render.
|
|
setImmediate(() => {
|
|
setFreezeState(freeze);
|
|
});
|
|
}
|
|
|
|
return <Freeze freeze={freeze ? freezeState : false}>{children}</Freeze>;
|
|
}
|
|
|
|
function ScreenStack(props: ScreenStackProps) {
|
|
const { children, ...rest } = props;
|
|
const size = React.Children.count(children);
|
|
// freezes all screens except the top one
|
|
const childrenWithFreeze = React.Children.map(children, (child, index) => {
|
|
// @ts-expect-error it's either SceneView in v6 or RouteView in v5
|
|
const { props, key } = child;
|
|
const descriptor = props?.descriptor ?? props?.descriptors?.[key];
|
|
const freezeEnabled = descriptor?.options?.freezeOnBlur ?? ENABLE_FREEZE;
|
|
|
|
return (
|
|
<DelayedFreeze freeze={freezeEnabled && size - index > 1}>
|
|
{child}
|
|
</DelayedFreeze>
|
|
);
|
|
});
|
|
|
|
return (
|
|
<ScreensNativeModules.NativeScreenStack {...rest}>
|
|
{childrenWithFreeze}
|
|
</ScreensNativeModules.NativeScreenStack>
|
|
);
|
|
}
|
|
|
|
// Incomplete type, all accessible properties available at:
|
|
// react-native/Libraries/Components/View/ReactNativeViewViewConfig.js
|
|
interface ViewConfig extends View {
|
|
viewConfig: {
|
|
validAttributes: {
|
|
style: {
|
|
display: boolean;
|
|
};
|
|
};
|
|
};
|
|
}
|
|
|
|
class InnerScreen extends React.Component<ScreenProps> {
|
|
private ref: React.ElementRef<typeof View> | null = null;
|
|
private closing = new Animated.Value(0);
|
|
private progress = new Animated.Value(0);
|
|
private goingForward = new Animated.Value(0);
|
|
|
|
setNativeProps(props: ScreenProps): void {
|
|
this.ref?.setNativeProps(props);
|
|
}
|
|
|
|
setRef = (ref: React.ElementRef<typeof View> | null): void => {
|
|
this.ref = ref;
|
|
this.props.onComponentRef?.(ref);
|
|
};
|
|
|
|
render() {
|
|
const {
|
|
enabled = ENABLE_SCREENS,
|
|
freezeOnBlur = ENABLE_FREEZE,
|
|
...rest
|
|
} = this.props;
|
|
|
|
if (enabled && isPlatformSupported) {
|
|
AnimatedNativeScreen =
|
|
AnimatedNativeScreen ||
|
|
Animated.createAnimatedComponent(ScreensNativeModules.NativeScreen);
|
|
|
|
let {
|
|
// Filter out active prop in this case because it is unused and
|
|
// can cause problems depending on react-native version:
|
|
// https://github.com/react-navigation/react-navigation/issues/4886
|
|
active,
|
|
activityState,
|
|
children,
|
|
isNativeStack,
|
|
gestureResponseDistance,
|
|
...props
|
|
} = rest;
|
|
|
|
if (active !== undefined && activityState === undefined) {
|
|
console.warn(
|
|
'It appears that you are using old version of react-navigation library. Please update @react-navigation/bottom-tabs, @react-navigation/stack and @react-navigation/drawer to version 5.10.0 or above to take full advantage of new functionality added to react-native-screens'
|
|
);
|
|
activityState = active !== 0 ? 2 : 0; // in the new version, we need one of the screens to have value of 2 after the transition
|
|
}
|
|
|
|
const handleRef = (ref: ViewConfig) => {
|
|
if (ref?.viewConfig?.validAttributes?.style) {
|
|
ref.viewConfig.validAttributes.style = {
|
|
...ref.viewConfig.validAttributes.style,
|
|
display: false,
|
|
};
|
|
this.setRef(ref);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<DelayedFreeze freeze={freezeOnBlur && activityState === 0}>
|
|
<AnimatedNativeScreen
|
|
{...props}
|
|
activityState={activityState}
|
|
gestureResponseDistance={{
|
|
start: gestureResponseDistance?.start ?? -1,
|
|
end: gestureResponseDistance?.end ?? -1,
|
|
top: gestureResponseDistance?.top ?? -1,
|
|
bottom: gestureResponseDistance?.bottom ?? -1,
|
|
}}
|
|
// This prevents showing blank screen when navigating between multiple screens with freezing
|
|
// https://github.com/software-mansion/react-native-screens/pull/1208
|
|
ref={handleRef}
|
|
onTransitionProgress={
|
|
!isNativeStack
|
|
? undefined
|
|
: Animated.event(
|
|
[
|
|
{
|
|
nativeEvent: {
|
|
progress: this.progress,
|
|
closing: this.closing,
|
|
goingForward: this.goingForward,
|
|
},
|
|
},
|
|
],
|
|
{ useNativeDriver: true }
|
|
)
|
|
}
|
|
>
|
|
{!isNativeStack ? ( // see comment of this prop in types.tsx for information why it is needed
|
|
children
|
|
) : (
|
|
<TransitionProgressContext.Provider
|
|
value={{
|
|
progress: this.progress,
|
|
closing: this.closing,
|
|
goingForward: this.goingForward,
|
|
}}
|
|
>
|
|
{children}
|
|
</TransitionProgressContext.Provider>
|
|
)}
|
|
</AnimatedNativeScreen>
|
|
</DelayedFreeze>
|
|
);
|
|
} else {
|
|
// same reason as above
|
|
let {
|
|
active,
|
|
activityState,
|
|
style,
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
onComponentRef,
|
|
...props
|
|
} = rest;
|
|
|
|
if (active !== undefined && activityState === undefined) {
|
|
activityState = active !== 0 ? 2 : 0;
|
|
}
|
|
return (
|
|
<Animated.View
|
|
style={[style, { display: activityState !== 0 ? 'flex' : 'none' }]}
|
|
ref={this.setRef}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function ScreenContainer(props: ScreenContainerProps) {
|
|
const { enabled = ENABLE_SCREENS, hasTwoStates, ...rest } = props;
|
|
|
|
if (enabled && isPlatformSupported) {
|
|
if (hasTwoStates) {
|
|
return <ScreensNativeModules.NativeScreenNavigationContainer {...rest} />;
|
|
}
|
|
return <ScreensNativeModules.NativeScreenContainer {...rest} />;
|
|
}
|
|
return <View {...rest} />;
|
|
}
|
|
|
|
function FullWindowOverlay(props: { children: ReactNode }) {
|
|
if (Platform.OS !== 'ios') {
|
|
console.warn('Importing FullWindowOverlay is only valid on iOS devices.');
|
|
return <View {...props} />;
|
|
}
|
|
return (
|
|
<ScreensNativeModules.NativeFullWindowOverlay
|
|
style={{ position: 'absolute', width: '100%', height: '100%' }}
|
|
>
|
|
{props.children}
|
|
</ScreensNativeModules.NativeFullWindowOverlay>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
headerSubview: {
|
|
position: 'absolute',
|
|
top: 0,
|
|
right: 0,
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
});
|
|
|
|
const ScreenStackHeaderBackButtonImage = (props: ImageProps): JSX.Element => (
|
|
<ScreensNativeModules.NativeScreenStackHeaderSubview
|
|
type="back"
|
|
style={styles.headerSubview}
|
|
>
|
|
<Image resizeMode="center" fadeDuration={0} {...props} />
|
|
</ScreensNativeModules.NativeScreenStackHeaderSubview>
|
|
);
|
|
|
|
const ScreenStackHeaderRightView = (
|
|
props: React.PropsWithChildren<ViewProps>
|
|
): JSX.Element => (
|
|
<ScreensNativeModules.NativeScreenStackHeaderSubview
|
|
{...props}
|
|
type="right"
|
|
style={styles.headerSubview}
|
|
/>
|
|
);
|
|
|
|
const ScreenStackHeaderLeftView = (
|
|
props: React.PropsWithChildren<ViewProps>
|
|
): JSX.Element => (
|
|
<ScreensNativeModules.NativeScreenStackHeaderSubview
|
|
{...props}
|
|
type="left"
|
|
style={styles.headerSubview}
|
|
/>
|
|
);
|
|
|
|
const ScreenStackHeaderCenterView = (
|
|
props: React.PropsWithChildren<ViewProps>
|
|
): JSX.Element => (
|
|
<ScreensNativeModules.NativeScreenStackHeaderSubview
|
|
{...props}
|
|
type="center"
|
|
style={styles.headerSubview}
|
|
/>
|
|
);
|
|
|
|
const ScreenStackHeaderSearchBarView = (
|
|
props: React.PropsWithChildren<SearchBarProps>
|
|
): JSX.Element => (
|
|
<ScreensNativeModules.NativeScreenStackHeaderSubview
|
|
{...props}
|
|
type="searchBar"
|
|
style={styles.headerSubview}
|
|
/>
|
|
);
|
|
|
|
export type {
|
|
StackPresentationTypes,
|
|
StackAnimationTypes,
|
|
BlurEffectTypes,
|
|
ScreenReplaceTypes,
|
|
ScreenOrientationTypes,
|
|
HeaderSubviewTypes,
|
|
ScreenProps,
|
|
ScreenContainerProps,
|
|
ScreenStackProps,
|
|
ScreenStackHeaderConfigProps,
|
|
SearchBarProps,
|
|
};
|
|
|
|
// context to be used when the user wants to use enhanced implementation
|
|
// e.g. to use `useReanimatedTransitionProgress` (see `reanimated` folder in repo)
|
|
const ScreenContext = React.createContext(InnerScreen);
|
|
|
|
class Screen extends React.Component<ScreenProps> {
|
|
static contextType = ScreenContext;
|
|
|
|
render() {
|
|
const ScreenWrapper = this.context || InnerScreen;
|
|
return <ScreenWrapper {...this.props} />;
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
// these are classes so they are not evaluated until used
|
|
// so no need to use getters for them
|
|
Screen,
|
|
ScreenContainer,
|
|
ScreenContext,
|
|
ScreenStack,
|
|
InnerScreen,
|
|
FullWindowOverlay,
|
|
|
|
get NativeScreen() {
|
|
return ScreensNativeModules.NativeScreen;
|
|
},
|
|
|
|
get NativeScreenContainer() {
|
|
return ScreensNativeModules.NativeScreenContainer;
|
|
},
|
|
|
|
get NativeScreenNavigationContainer() {
|
|
return ScreensNativeModules.NativeScreenNavigationContainer;
|
|
},
|
|
|
|
get ScreenStackHeaderConfig() {
|
|
return ScreensNativeModules.NativeScreenStackHeaderConfig;
|
|
},
|
|
get ScreenStackHeaderSubview() {
|
|
return ScreensNativeModules.NativeScreenStackHeaderSubview;
|
|
},
|
|
get SearchBar() {
|
|
if (!isSearchBarAvailableForCurrentPlatform) {
|
|
console.warn(
|
|
'Importing SearchBar is only valid on iOS and Android devices.'
|
|
);
|
|
return View;
|
|
}
|
|
|
|
return ScreensNativeModules.NativeSearchBar;
|
|
},
|
|
// these are functions and will not be evaluated until used
|
|
// so no need to use getters for them
|
|
ScreenStackHeaderBackButtonImage,
|
|
ScreenStackHeaderRightView,
|
|
ScreenStackHeaderLeftView,
|
|
ScreenStackHeaderCenterView,
|
|
ScreenStackHeaderSearchBarView,
|
|
|
|
enableScreens,
|
|
enableFreeze,
|
|
screensEnabled,
|
|
shouldUseActivityState,
|
|
useTransitionProgress,
|
|
|
|
isSearchBarAvailableForCurrentPlatform,
|
|
executeNativeBackPress,
|
|
};
|