From eedbb0fa16066fb093e758ed15b5601058776193 Mon Sep 17 00:00:00 2001 From: Maciej Stosio Date: Mon, 4 Nov 2024 22:53:38 +0100 Subject: [PATCH 1/8] refactor: move ScreenGestureDetector to ScreenStack (#2449) ## Description This PR moves logic required by Custom Screen Transition to react-native-screens from react-navigation (see https://github.com/react-navigation/react-navigation/pull/11943). Instead of wrapping ScreenStack in NativeStack in react-navigation, we would add it to ScreenStack. ## Testing See Example App > Swipe Back Animation (changes from react-navigation required: https://github.com/react-navigation/react-navigation/pull/12204) ## Checklist - [x] Included code example that can be used to test this change - [x] Updated TS types - [ ] Updated documentation: - [ ] https://github.com/software-mansion/react-native-screens/blob/main/guides/GUIDE_FOR_LIBRARY_AUTHORS.md - [ ] https://github.com/software-mansion/react-native-screens/blob/main/native-stack/README.md - [ ] https://github.com/software-mansion/react-native-screens/blob/main/src/types.tsx - [ ] https://github.com/software-mansion/react-native-screens/blob/main/src/native-stack/types.tsx - [x] Ensured that CI passes --- src/components/ScreenStack.tsx | 108 +++++++++++++++--- src/contexts.tsx | 6 + .../GestureDetectorProvider.tsx | 4 +- src/gesture-handler/ScreenGestureDetector.tsx | 8 +- src/index.tsx | 5 - src/native-stack/types.tsx | 4 + src/native-stack/views/NativeStackView.tsx | 54 +++------ src/types.tsx | 71 +++++++++++- 8 files changed, 190 insertions(+), 70 deletions(-) create mode 100644 src/contexts.tsx diff --git a/src/components/ScreenStack.tsx b/src/components/ScreenStack.tsx index d1eb86b0cd..205c402a36 100644 --- a/src/components/ScreenStack.tsx +++ b/src/components/ScreenStack.tsx @@ -1,9 +1,16 @@ 'use client'; -import React from 'react'; -import { ScreenStackProps } from '../types'; +import React, { PropsWithChildren } from 'react'; +import { + GestureDetectorBridge, + GestureProviderProps, + GoBackGesture, + ScreenStackProps, +} from '../types'; +import { GHContext } from '../contexts'; import { freezeEnabled } from '../core'; import DelayedFreeze from './helpers/DelayedFreeze'; +import warnOnce from 'warn-once'; // Native components import ScreenStackNativeComponent, { @@ -14,10 +21,58 @@ function isFabric() { return 'nativeFabricUIManager' in global; } +const assertGHProvider = ( + ScreenGestureDetector: ( + props: PropsWithChildren, + ) => React.JSX.Element, + goBackGesture: GoBackGesture | undefined, +) => { + const isGestureDetectorProviderNotDetected = + ScreenGestureDetector.name !== 'GHWrapper' && goBackGesture !== undefined; + + warnOnce( + isGestureDetectorProviderNotDetected, + 'Cannot detect GestureDetectorProvider in a screen that uses `goBackGesture`. Make sure your navigator is wrapped in GestureDetectorProvider.', + ); +}; + +const assertCustomScreenTransitionsProps = ( + screensRefs: ScreenStackProps['screensRefs'], + currentScreenId: ScreenStackProps['currentScreenId'], + goBackGesture: ScreenStackProps['goBackGesture'], +) => { + const isGestureDetectorNotConfiguredProperly = + goBackGesture !== undefined && + screensRefs === undefined && + currentScreenId === undefined; + + warnOnce( + isGestureDetectorNotConfiguredProperly, + 'Custom Screen Transition require screensRefs and currentScreenId to be provided.', + ); +}; + function ScreenStack(props: ScreenStackProps) { - const { children, gestureDetectorBridge, ...rest } = props; + const { + goBackGesture, + screensRefs, + currentScreenId, + transitionAnimation, + screenEdgeGesture, + onFinishTransitioning, + children, + ...rest + } = props; + const ref = React.useRef(null); const size = React.Children.count(children); + const ScreenGestureDetector = React.useContext(GHContext); + const gestureDetectorBridge = React.useRef({ + stackUseEffectCallback: _stackRef => { + // this method will be overriden in GestureDetector + }, + }); + // 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 @@ -40,24 +95,39 @@ function ScreenStack(props: ScreenStackProps) { }); React.useEffect(() => { - if (gestureDetectorBridge) { - gestureDetectorBridge.current.stackUseEffectCallback(ref); - } + gestureDetectorBridge.current.stackUseEffectCallback(ref); }); + + assertGHProvider(ScreenGestureDetector, goBackGesture); + + assertCustomScreenTransitionsProps( + screensRefs, + currentScreenId, + goBackGesture, + ); + return ( - - {childrenWithFreeze} - + + + {childrenWithFreeze} + + ); } diff --git a/src/contexts.tsx b/src/contexts.tsx new file mode 100644 index 0000000000..2b95c20a22 --- /dev/null +++ b/src/contexts.tsx @@ -0,0 +1,6 @@ +import React, { PropsWithChildren } from 'react'; +import { GestureProviderProps } from './types'; + +export const GHContext = React.createContext( + (props: PropsWithChildren) => <>{props.children}, +); diff --git a/src/gesture-handler/GestureDetectorProvider.tsx b/src/gesture-handler/GestureDetectorProvider.tsx index eb19172a92..8aab787885 100644 --- a/src/gesture-handler/GestureDetectorProvider.tsx +++ b/src/gesture-handler/GestureDetectorProvider.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import { GHContext } from '../native-stack/contexts/GHContext'; +import { GestureProviderProps } from '../types'; +import { GHContext } from '../contexts'; import ScreenGestureDetector from './ScreenGestureDetector'; -import type { GestureProviderProps } from '../native-stack/types'; function GHWrapper(props: GestureProviderProps) { return ; diff --git a/src/gesture-handler/ScreenGestureDetector.tsx b/src/gesture-handler/ScreenGestureDetector.tsx index b5e8670426..dc4c7c3816 100644 --- a/src/gesture-handler/ScreenGestureDetector.tsx +++ b/src/gesture-handler/ScreenGestureDetector.tsx @@ -14,7 +14,6 @@ import { makeMutable, runOnUI, } from 'react-native-reanimated'; -import type { GestureProviderProps } from 'src/native-stack/types'; import { getShadowNodeWrapperAndTagFromRef, isFabric } from './fabricUtils'; import { RNScreensTurboModule } from './RNScreensTurboModule'; import { DefaultEvent, DefaultScreenDimensions } from './defaults'; @@ -23,6 +22,7 @@ import { checkIfTransitionCancelled, getAnimationForTransition, } from './constraints'; +import { GestureProviderProps } from '../types'; const EmptyGestureHandler = Gesture.Fling(); @@ -33,7 +33,7 @@ const ScreenGestureDetector = ({ screenEdgeGesture, transitionAnimation: customTransitionAnimation, screensRefs, - currentRouteKey, + currentScreenId, }: GestureProviderProps) => { const sharedEvent = useSharedValue(DefaultEvent); const startingGesturePosition = useSharedValue(DefaultEvent); @@ -73,7 +73,7 @@ const ScreenGestureDetector = ({ }; useEffect(() => { - if (!IS_FABRIC || !goBackGesture) { + if (!IS_FABRIC || !goBackGesture || screensRefs === undefined) { return; } const screenTagToNodeWrapper: Record> = {}; @@ -87,7 +87,7 @@ const ScreenGestureDetector = ({ } } screenTagToNodeWrapperUI.value = screenTagToNodeWrapper; - }, [currentRouteKey, goBackGesture]); + }, [currentScreenId, goBackGesture]); function computeProgress( event: GestureUpdateEvent, diff --git a/src/index.tsx b/src/index.tsx index 76fd648d29..0403427f1f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -41,11 +41,6 @@ export { default as FullWindowOverlay } from './components/FullWindowOverlay'; export { default as ScreenFooter } from './components/ScreenFooter'; export { default as ScreenContentWrapper } from './components/ScreenContentWrapper'; -/** - * Contexts - */ -export { GHContext } from './native-stack/contexts/GHContext'; - /** * Utils */ diff --git a/src/native-stack/types.tsx b/src/native-stack/types.tsx index 5f319d108e..da7a020a31 100644 --- a/src/native-stack/types.tsx +++ b/src/native-stack/types.tsx @@ -564,6 +564,10 @@ export type NativeStackDescriptorMap = { [key: string]: NativeStackDescriptor; }; +/** + * Those below copied to src/types.ts should be removed with next minor and native-stack v5 removal + */ + /** * copy from GestureHandler to avoid strong dependency * @deprecated NativeStack has been moved from react-native-screens/native-stack to @react-navigation/native since version v6. With react-native-screens v4 native stack v5 (react-native-screens/native-stack) is deprecated and marked for removal in the upcoming minor release, react-native-screens v4 will support only @react-navigation/native-stack v7. diff --git a/src/native-stack/views/NativeStackView.tsx b/src/native-stack/views/NativeStackView.tsx index 6ffe4a20a3..6066541cfd 100644 --- a/src/native-stack/views/NativeStackView.tsx +++ b/src/native-stack/views/NativeStackView.tsx @@ -11,10 +11,9 @@ import { // eslint-disable-next-line import/no-named-as-default, import/default, import/no-named-as-default-member, import/namespace import AppContainer from 'react-native/Libraries/ReactNative/AppContainer'; import warnOnce from 'warn-once'; -import { StackPresentationTypes, GestureDetectorBridge } from '../../types'; +import { StackPresentationTypes } from '../../types'; import ScreenStack from '../../components/ScreenStack'; import ScreenContentWrapper from '../../components/ScreenContentWrapper'; -import { GHContext } from '../contexts/GHContext'; import { ScreenContext } from '../../components/Screen'; import { ParamListBase, @@ -477,53 +476,32 @@ function NativeStackViewInner({ const currentRouteKey = routes[state.index].key; const { goBackGesture, transitionAnimation, screenEdgeGesture } = descriptors[currentRouteKey].options; - const gestureDetectorBridge = React.useRef({ - stackUseEffectCallback: _stackRef => { - // this method will be override in GestureDetector - }, - }); type RefHolder = Record< string, React.MutableRefObject> >; const screensRefs = React.useRef({}); - const ScreenGestureDetector = React.useContext(GHContext); - - React.useEffect(() => { - if ( - ScreenGestureDetector.name !== 'GHWrapper' && - goBackGesture !== undefined - ) { - console.warn( - 'Cannot detect GestureDetectorProvider in a screen that uses `goBackGesture`. Make sure your navigator is wrapped in GestureDetectorProvider.', - ); - } - }, [ScreenGestureDetector.name, goBackGesture]); return ( - - - {routes.map((route, index) => ( - - ))} - - + currentScreenId={currentRouteKey}> + {routes.map((route, index) => ( + + ))} + ); } diff --git a/src/types.tsx b/src/types.tsx index 162ada4161..f746b6ba28 100644 --- a/src/types.tsx +++ b/src/types.tsx @@ -487,13 +487,12 @@ export interface GestureDetectorBridge { ) => void; } -export interface ScreenStackProps extends ViewProps { +export interface ScreenStackProps extends ViewProps, GestureProps { children?: React.ReactNode; /** * A callback that gets called when the current screen finishes its transition. */ onFinishTransitioning?: (e: NativeSyntheticEvent) => void; - gestureDetectorBridge?: React.MutableRefObject; ref?: React.MutableRefObject>; } @@ -799,3 +798,71 @@ export interface SearchBarProps { */ shouldShowHintSearchIcon?: boolean; } + +/** + * Custom Screen Transition + */ + +/** + * copy from GestureHandler to avoid strong dependency + */ +export type PanGestureHandlerEventPayload = { + x: number; + y: number; + absoluteX: number; + absoluteY: number; + translationX: number; + translationY: number; + velocityX: number; + velocityY: number; +}; + +/** + * copy from Reanimated to avoid strong dependency + */ +export type GoBackGesture = + | 'swipeRight' + | 'swipeLeft' + | 'swipeUp' + | 'swipeDown' + | 'verticalSwipe' + | 'horizontalSwipe' + | 'twoDimensionalSwipe'; + +export interface MeasuredDimensions { + x: number; + y: number; + width: number; + height: number; + pageX: number; + pageY: number; +} + +export type AnimatedScreenTransition = { + topScreenStyle: ( + event: PanGestureHandlerEventPayload, + screenSize: MeasuredDimensions, + ) => Record; + belowTopScreenStyle: ( + event: PanGestureHandlerEventPayload, + screenSize: MeasuredDimensions, + ) => Record; +}; + +export type ScreensRefsHolder = Record< + string, + React.MutableRefObject> +>; + +export interface GestureProps { + screensRefs?: React.MutableRefObject; + currentScreenId?: string; + goBackGesture?: GoBackGesture; + transitionAnimation?: AnimatedScreenTransition; + screenEdgeGesture?: boolean; +} + +export interface GestureProviderProps extends GestureProps { + children?: React.ReactNode; + gestureDetectorBridge: React.MutableRefObject; +} From 5a2e4ebab50f2774b3810d7ce34ae176f2419be5 Mon Sep 17 00:00:00 2001 From: Maciej Stosio Date: Tue, 5 Nov 2024 11:10:17 +0100 Subject: [PATCH 2/8] refactor!: use context instead of screenRefs (#2476) ## Description This PR replaces passing screenRefs into using context. This avoids unnecessary logic on react-navigation side. ## Changes Create Context that holds screen refs and add this ref in ScreenStackItem instead of doing same on react-navigation part. See the changes there: https://github.com/react-navigation/react-navigation/pull/12225 ## Testing See Example App > Swipe Back Animation (changes from react-navigation required: https://github.com/react-navigation/react-navigation/pull/12225) OR react-navigation: TestScreenAnimation.tsx native-stack v5: TestScreenAnimationV5.tsx |native-stack v5|react-navigation| |-|-| |