diff --git a/.eslintrc.js b/.eslintrc.js
index 2fe32e7f1b..aa4543e589 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -18,6 +18,7 @@ module.exports = {
'react-native-screens',
'react-native-screens/native-stack',
'react-native-screens/reanimated',
+ 'react-native-screens/gesture-handler',
],
'import/ignore': [
'node_modules/react-native/index\\.js$',
diff --git a/Example/App.tsx b/Example/App.tsx
index 3af0e5e483..f53e19b350 100644
--- a/Example/App.tsx
+++ b/Example/App.tsx
@@ -15,6 +15,7 @@ import RNRestart from 'react-native-restart';
import { ListItem, SettingsSwitch } from './src/shared';
import SimpleNativeStack from './src/screens/SimpleNativeStack';
+import SwipeBackAnimation from './src/screens/SwipeBackAnimation';
import StackPresentation from './src/screens/StackPresentation';
import HeaderOptions from './src/screens/HeaderOptions';
import StatusBarExample from './src/screens/StatusBar';
@@ -27,6 +28,8 @@ import Events from './src/screens/Events';
import Gestures from './src/screens/Gestures';
import { enableFreeze } from 'react-native-screens';
+import { GestureDetectorProvider } from 'react-native-screens/gesture-handler';
+import { GestureHandlerRootView } from 'react-native-gesture-handler';
enableFreeze();
@@ -47,6 +50,11 @@ const SCREENS: Record<
component: SimpleNativeStack,
type: 'example',
},
+ SwipeBackAnimation: {
+ title: 'Swipe Back Animation',
+ component: SwipeBackAnimation,
+ type: 'example',
+ },
StackPresentation: {
title: 'Stack Presentation',
component: StackPresentation,
@@ -150,26 +158,30 @@ const MainScreen = ({ navigation }: MainScreenProps): JSX.Element => (
);
const ExampleApp = (): JSX.Element => (
-
-
-
- {Object.keys(SCREENS).map(name => (
- SCREENS[name].component}
- options={{ headerShown: false }}
- />
- ))}
-
-
+
+
+
+
+
+ {Object.keys(SCREENS).map(name => (
+ SCREENS[name].component}
+ options={{ headerShown: false }}
+ />
+ ))}
+
+
+
+
);
const styles = StyleSheet.create({
diff --git a/Example/babel.config.js b/Example/babel.config.js
index 5fe62b3c53..983e075de7 100644
--- a/Example/babel.config.js
+++ b/Example/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
- plugins: [],
+ plugins: ['react-native-reanimated/plugin'],
};
diff --git a/Example/ios/Podfile.lock b/Example/ios/Podfile.lock
index 2a14448303..098b0214ed 100644
--- a/Example/ios/Podfile.lock
+++ b/Example/ios/Podfile.lock
@@ -489,8 +489,12 @@ PODS:
- React-jsi (= 0.72.4)
- React-logger (= 0.72.4)
- React-perflogger (= 0.72.4)
- - RNGestureHandler (2.12.1):
+ - RNGestureHandler (2.13.1):
- React-Core
+ - RNReanimated (3.7.0-nightly-20240109-9e2c33716):
+ - RCT-Folly (= 2021.07.22.00)
+ - React-Core
+ - ReactCommon/turbomodule/core
- RNScreens (3.29.0):
- RCT-Folly (= 2021.07.22.00)
- React-Core
@@ -567,6 +571,7 @@ DEPENDENCIES:
- React-utils (from `../node_modules/react-native/ReactCommon/react/utils`)
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
+ - RNReanimated (from `../node_modules/react-native-reanimated`)
- RNScreens (from `../node_modules/react-native-screens`)
- RNVectorIcons (from `../node_modules/react-native-vector-icons`)
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
@@ -672,6 +677,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon"
RNGestureHandler:
:path: "../node_modules/react-native-gesture-handler"
+ RNReanimated:
+ :path: "../node_modules/react-native-reanimated"
RNScreens:
:path: "../node_modules/react-native-screens"
RNVectorIcons:
@@ -680,9 +687,9 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/yoga"
SPEC CHECKSUMS:
- boost: 57d2868c099736d80fcd648bf211b4431e51a558
+ boost: a7c83b31436843459a1961bfd74b96033dc77234
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
- DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
+ DoubleConversion: 831926d9b8bf8166fd87886c4abab286c2422662
FBLazyVector: 5d4a3b7f411219a45a6d952f77d2c0a6c9989da5
FBReactNativeSpec: 3fc2d478e1c4b08276f9dd9128f80ec6d5d85c1f
Flipper: 6edb735e6c3e332975d1b17956bcc584eccf5818
@@ -694,7 +701,7 @@ SPEC CHECKSUMS:
Flipper-PeerTalk: 116d8f857dc6ef55c7a5a75ea3ceaafe878aadc9
FlipperKit: 2efad7007d6745a3f95e4034d547be637f89d3f6
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
- glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b
+ glog: 5337263514dd6f09803962437687240c5dc39aa4
hermes-engine: 81191603c4eaa01f5e4ae5737a9efcf64756c7b2
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c
@@ -732,8 +739,9 @@ SPEC CHECKSUMS:
React-runtimescheduler: 4941cc1b3cf08b792fbf666342c9fc95f1969035
React-utils: b79f2411931f9d3ea5781404dcbb2fa8a837e13a
ReactCommon: 4b2bdcb50a3543e1c2b2849ad44533686610826d
- RNGestureHandler: c0d04458598fcb26052494ae23dda8f8f5162b13
- RNScreens: 3c5b9f4a9dcde752466854b6109b79c0e205dad3
+ RNGestureHandler: 38aa38413896620338948fbb5c90579a7b1c3fde
+ RNReanimated: 0f8173d46f45c2f690c416dff10206832631571d
+ RNScreens: 975abd21a20b6ebd26563e5ab86b30250569c182
RNVectorIcons: 31cebfcf94e8cf8686eb5303ae0357da64d7a5a4
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
Yoga: 3efc43e0d48686ce2e8c60f99d4e6bd349aff981
@@ -741,4 +749,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 86e380a4262db238c7a45428750af2d88465585c
-COCOAPODS: 1.11.3
+COCOAPODS: 1.14.3
diff --git a/Example/metro.config.js b/Example/metro.config.js
index 5afc0308f2..12a53b6e40 100644
--- a/Example/metro.config.js
+++ b/Example/metro.config.js
@@ -10,6 +10,8 @@ const root = path.resolve(__dirname, '..');
const modules = [
'@react-navigation/native',
'react-native-safe-area-context',
+ 'react-native-gesture-handler',
+ 'react-native-reanimated',
...Object.keys(pack.peerDependencies),
];
diff --git a/Example/package.json b/Example/package.json
index aa330a8fde..ff12370b49 100644
--- a/Example/package.json
+++ b/Example/package.json
@@ -23,7 +23,8 @@
"nanoid": "^4.0.2",
"react": "18.2.0",
"react-native": "0.72.4",
- "react-native-gesture-handler": "^2.12.1",
+ "react-native-gesture-handler": "^2.13.1",
+ "react-native-reanimated": "3.7.0-nightly-20240109-9e2c33716",
"react-native-restart": "^0.0.27",
"react-native-safe-area-context": "^4.8.1",
"react-native-screens": "link:../",
@@ -36,9 +37,9 @@
"@react-native/eslint-config": "^0.72.2",
"@react-native/metro-config": "^0.72.11",
"@tsconfig/react-native": "^3.0.0",
+ "@types/jest": "^29.2.5",
"@types/react": "^18.0.24",
"@types/react-native": "0.72.2",
- "@types/jest": "^29.2.5",
"@types/react-native-restart": "^0.0.0",
"@types/react-test-renderer": "^18.0.0",
"babel-jest": "^29.3.1",
diff --git a/Example/src/screens/SwipeBackAnimation.tsx b/Example/src/screens/SwipeBackAnimation.tsx
new file mode 100644
index 0000000000..526765c76f
--- /dev/null
+++ b/Example/src/screens/SwipeBackAnimation.tsx
@@ -0,0 +1,74 @@
+import React from 'react';
+import { View, StyleSheet, I18nManager } from 'react-native';
+import {
+ createNativeStackNavigator,
+ NativeStackNavigationProp,
+} from 'react-native-screens/native-stack';
+import { Button } from '../shared';
+
+type StackParamList = {
+ ScreenA: undefined;
+ ScreenB: undefined;
+ ScreenC: undefined;
+};
+
+interface MainScreenProps {
+ navigation: NativeStackNavigationProp;
+}
+
+const MainScreen = ({ navigation }: MainScreenProps): JSX.Element => (
+
+
+);
+
+interface ScreenBProps {
+ navigation: NativeStackNavigationProp;
+}
+
+const ScreenB = ({ navigation }: ScreenBProps): JSX.Element => (
+
+ navigation.navigate('ScreenC')} />
+ navigation.goBack()} />
+
+);
+
+interface ScreenCProps {
+ navigation: NativeStackNavigationProp;
+}
+
+const ScreenC = ({ navigation }: ScreenCProps): JSX.Element => (
+
+ navigation.goBack()} />
+
+);
+
+const Stack = createNativeStackNavigator();
+
+const App = (): JSX.Element => (
+
+
+
+
+
+);
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ paddingTop: 10,
+ },
+});
+
+export default App;
diff --git a/Example/yarn.lock b/Example/yarn.lock
index 7e96b489cc..20cfe1b557 100644
--- a/Example/yarn.lock
+++ b/Example/yarn.lock
@@ -118,6 +118,21 @@
"@babel/helper-split-export-declaration" "^7.22.6"
semver "^6.3.1"
+"@babel/helper-create-class-features-plugin@^7.22.15":
+ version "7.22.15"
+ resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz#97a61b385e57fe458496fad19f8e63b63c867de4"
+ integrity sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==
+ dependencies:
+ "@babel/helper-annotate-as-pure" "^7.22.5"
+ "@babel/helper-environment-visitor" "^7.22.5"
+ "@babel/helper-function-name" "^7.22.5"
+ "@babel/helper-member-expression-to-functions" "^7.22.15"
+ "@babel/helper-optimise-call-expression" "^7.22.5"
+ "@babel/helper-replace-supers" "^7.22.9"
+ "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5"
+ "@babel/helper-split-export-declaration" "^7.22.6"
+ semver "^6.3.1"
+
"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.22.5":
version "7.22.9"
resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.9.tgz#9d8e61a8d9366fe66198f57c40565663de0825f6"
@@ -171,6 +186,13 @@
dependencies:
"@babel/types" "^7.22.5"
+"@babel/helper-member-expression-to-functions@^7.22.15":
+ version "7.23.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz#9263e88cc5e41d39ec18c9a3e0eced59a3e7d366"
+ integrity sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==
+ dependencies:
+ "@babel/types" "^7.23.0"
+
"@babel/helper-member-expression-to-functions@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz#0a7c56117cad3372fbf8d2fb4bf8f8d64a1e76b2"
@@ -178,6 +200,13 @@
dependencies:
"@babel/types" "^7.22.5"
+"@babel/helper-module-imports@^7.22.15":
+ version "7.22.15"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0"
+ integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==
+ dependencies:
+ "@babel/types" "^7.22.15"
+
"@babel/helper-module-imports@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz#1a8f4c9f4027d23f520bd76b364d44434a72660c"
@@ -196,6 +225,17 @@
"@babel/helper-split-export-declaration" "^7.22.6"
"@babel/helper-validator-identifier" "^7.22.5"
+"@babel/helper-module-transforms@^7.23.0":
+ version "7.23.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz#3ec246457f6c842c0aee62a01f60739906f7047e"
+ integrity sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==
+ dependencies:
+ "@babel/helper-environment-visitor" "^7.22.20"
+ "@babel/helper-module-imports" "^7.22.15"
+ "@babel/helper-simple-access" "^7.22.5"
+ "@babel/helper-split-export-declaration" "^7.22.6"
+ "@babel/helper-validator-identifier" "^7.22.20"
+
"@babel/helper-optimise-call-expression@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz#f21531a9ccbff644fdd156b4077c16ff0c3f609e"
@@ -262,6 +302,11 @@
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193"
integrity sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==
+"@babel/helper-validator-option@^7.22.15":
+ version "7.22.15"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz#694c30dfa1d09a6534cdfcafbe56789d36aba040"
+ integrity sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==
+
"@babel/helper-validator-option@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz#de52000a15a177413c8234fa3a8af4ee8102d0ac"
@@ -754,6 +799,15 @@
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/helper-simple-access" "^7.22.5"
+"@babel/plugin-transform-modules-commonjs@^7.23.0":
+ version "7.23.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.0.tgz#b3dba4757133b2762c00f4f94590cf6d52602481"
+ integrity sha512-32Xzss14/UVc7k9g775yMIvkVK8xwKE0DPdP5JTapr3+Z9w4tzeOuLNY6BXDQR6BdnzIlXnCGAzsk/ICHBLVWQ==
+ dependencies:
+ "@babel/helper-module-transforms" "^7.23.0"
+ "@babel/helper-plugin-utils" "^7.22.5"
+ "@babel/helper-simple-access" "^7.22.5"
+
"@babel/plugin-transform-modules-systemjs@^7.22.11":
version "7.22.11"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.11.tgz#3386be5875d316493b517207e8f1931d93154bb1"
@@ -803,6 +857,13 @@
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/plugin-syntax-numeric-separator" "^7.10.4"
+"@babel/plugin-transform-object-assign@^7.16.7":
+ version "7.22.5"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-assign/-/plugin-transform-object-assign-7.22.5.tgz#290c1b9555dcea48bb2c29ad94237777600d04f9"
+ integrity sha512-iDhx9ARkXq4vhZ2CYOSnQXkmxkDgosLi3J8Z17mKz7LyzthtkdVchLD7WZ3aXeCuvJDOW3+1I5TpJmwIbF9MKQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.22.5"
+
"@babel/plugin-transform-object-rest-spread@^7.22.11":
version "7.22.11"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.11.tgz#dbbb06ce783cd994a8f430d8cefa553e9b42ca62"
@@ -976,6 +1037,16 @@
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/plugin-syntax-typescript" "^7.22.5"
+"@babel/plugin-transform-typescript@^7.22.15":
+ version "7.22.15"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.22.15.tgz#15adef906451d86349eb4b8764865c960eb54127"
+ integrity sha512-1uirS0TnijxvQLnlv5wQBwOX3E1wCFX7ITv+9pBV2wKEk4K+M5tqDaoNXnTH8tjEIYHLO98MwiTWO04Ggz4XuA==
+ dependencies:
+ "@babel/helper-annotate-as-pure" "^7.22.5"
+ "@babel/helper-create-class-features-plugin" "^7.22.15"
+ "@babel/helper-plugin-utils" "^7.22.5"
+ "@babel/plugin-syntax-typescript" "^7.22.5"
+
"@babel/plugin-transform-unicode-escapes@^7.22.10":
version "7.22.10"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz#c723f380f40a2b2f57a62df24c9005834c8616d9"
@@ -1122,6 +1193,17 @@
"@babel/plugin-transform-modules-commonjs" "^7.22.11"
"@babel/plugin-transform-typescript" "^7.22.11"
+"@babel/preset-typescript@^7.16.7":
+ version "7.23.0"
+ resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.23.0.tgz#cc6602d13e7e5b2087c811912b87cf937a9129d9"
+ integrity sha512-6P6VVa/NM/VlAYj5s2Aq/gdVg8FSENCg3wlZ6Qau9AcPaoF5LbN1nyGlR9DTRIw9PpxI94e+ReydsJHcjwAweg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.22.5"
+ "@babel/helper-validator-option" "^7.22.15"
+ "@babel/plugin-syntax-jsx" "^7.22.5"
+ "@babel/plugin-transform-modules-commonjs" "^7.23.0"
+ "@babel/plugin-transform-typescript" "^7.22.15"
+
"@babel/register@^7.13.16":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.22.5.tgz#e4d8d0f615ea3233a27b5c6ada6750ee59559939"
@@ -6740,10 +6822,10 @@ react-native-codegen@^0.71.3:
jscodeshift "^0.13.1"
nullthrows "^1.1.1"
-react-native-gesture-handler@^2.12.1:
- version "2.12.1"
- resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.12.1.tgz#f11a99fb95169810c6886fad5efa01a17fd81660"
- integrity sha512-deqh36bw82CFUV9EC4tTo2PP1i9HfCOORGS3Zmv71UYhEZEHkzZv18IZNPB+2Awzj45vLIidZxGYGFxHlDSQ5A==
+react-native-gesture-handler@^2.13.1:
+ version "2.13.1"
+ resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.13.1.tgz#bad89caacd62c4560b9953b02f85f37ee42d5d4c"
+ integrity sha512-hW454X7sjuiBN+lobqw63pmT3boAmTl5OP6zQLq83iEe4T6PcHZ9lxzgCrebtgmutY8cJfq9rM2dOUVh9WBcww==
dependencies:
"@egjs/hammerjs" "^2.0.17"
hoist-non-react-statics "^3.3.0"
@@ -6756,6 +6838,16 @@ react-native-iphone-x-helper@^1.3.0:
resolved "https://registry.yarnpkg.com/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz#20c603e9a0e765fd6f97396638bdeb0e5a60b010"
integrity sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg==
+react-native-reanimated@3.7.0-nightly-20240109-9e2c33716:
+ version "3.7.0-nightly-20240109-9e2c33716"
+ resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-3.7.0-nightly-20240109-9e2c33716.tgz#ec647feabf1a0ba6673830f6ee2f3d57e0aa63b7"
+ integrity sha512-HMhmzQmAglaHuWwTiMVKx3fKZURi8oVP4u/76KYNBvUDK1Vh+sR54aFQ6d3X877LObw+R9vgr8LmajisY4poVQ==
+ dependencies:
+ "@babel/plugin-transform-object-assign" "^7.16.7"
+ "@babel/preset-typescript" "^7.16.7"
+ convert-source-map "^2.0.0"
+ invariant "^2.2.4"
+
react-native-restart@^0.0.27:
version "0.0.27"
resolved "https://registry.yarnpkg.com/react-native-restart/-/react-native-restart-0.0.27.tgz#43aa8210312c9dfa5ec7bd4b2f35238ad7972b19"
diff --git a/FabricTestExample/App.js b/FabricTestExample/App.js
index 189443a165..eec0fb0e8d 100644
--- a/FabricTestExample/App.js
+++ b/FabricTestExample/App.js
@@ -90,6 +90,7 @@ import Test1802 from './src/Test1802';
import Test1829 from './src/Test1829';
import Test1844 from './src/Test1844';
import Test1864 from './src/Test1864';
+import TestScreenAnimation from './src/TestScreenAnimation';
import Test1981 from './src/Test1981';
enableFreeze(true);
diff --git a/FabricTestExample/ios/Podfile.lock b/FabricTestExample/ios/Podfile.lock
index 88af5aedd4..98e67a3749 100644
--- a/FabricTestExample/ios/Podfile.lock
+++ b/FabricTestExample/ios/Podfile.lock
@@ -1231,7 +1231,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- - RNReanimated (3.6.0):
+ - RNReanimated (3.7.0-nightly-20240109-9e2c33716):
- glog
- hermes-engine
- RCT-Folly (= 2022.05.16.00)
@@ -1506,7 +1506,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
boost: 26fad476bfa736552bbfa698a06cc530475c1505
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
- DoubleConversion: fea03f2699887d960129cc54bba7e52542b6f953
+ DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
FBLazyVector: 39ba45baf4e398618f8b3a4bb6ba8fcdb7fc2133
Flipper: c7a0093234c4bdd456e363f2f19b2e4b27652d44
Flipper-Boost-iOSX: fd1e2b8cbef7e662a122412d7ac5f5bea715403c
@@ -1517,7 +1517,7 @@ SPEC CHECKSUMS:
Flipper-PeerTalk: 116d8f857dc6ef55c7a5a75ea3ceaafe878aadc9
FlipperKit: 37525a5d056ef9b93d1578e04bc3ea1de940094f
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
- glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2
+ glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b
hermes-engine: 7cea8d9de082031f5e81d491d1e346e4eeca1699
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c
@@ -1568,11 +1568,11 @@ SPEC CHECKSUMS:
React-utils: 42708ea436853045ef1eaff29996813d9fbbe209
ReactCommon: 851280fb976399ca1aabc74cc2c3612069ea70a2
RNGestureHandler: 38016feaff9bd5d8282c78ddce37a89b3a1d595b
- RNReanimated: 513822d418b77dc528cbc7a4e6621fcb29a5c1ea
- RNScreens: f7b8bb892b4957f6f91e5dfd9a191e7f13ce8baa
+ RNReanimated: c70748905d6263c7591f7e20356635ce13c9e522
+ RNScreens: 42f2b81fffc251443d43275f495073ab965ba0d9
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
- Yoga: 20d6a900dcc8d61d5e3b799bbf627cc34474a8c4
+ Yoga: 44003f970aa541b79dfdd59cf236fda41bd5890f
PODFILE CHECKSUM: f0682954c3112ba9068a9640979866030d94959c
-COCOAPODS: 1.11.3
+COCOAPODS: 1.14.3
diff --git a/FabricTestExample/metro.config.js b/FabricTestExample/metro.config.js
index 00f71a32ee..d090c69c47 100644
--- a/FabricTestExample/metro.config.js
+++ b/FabricTestExample/metro.config.js
@@ -12,6 +12,7 @@ const modules = [
'@react-navigation/stack',
'react-native-reanimated',
'react-native-safe-area-context',
+ 'react-native-gesture-handler',
...Object.keys(pack.peerDependencies),
];
diff --git a/FabricTestExample/package.json b/FabricTestExample/package.json
index b473036ee4..cc3335559a 100644
--- a/FabricTestExample/package.json
+++ b/FabricTestExample/package.json
@@ -18,7 +18,7 @@
"react": "18.2.0",
"react-native": "0.73.0",
"react-native-gesture-handler": "^2.12.1",
- "react-native-reanimated": "^3.6.0",
+ "react-native-reanimated": "3.7.0-nightly-20240109-9e2c33716",
"react-native-safe-area-context": "^4.8.1",
"react-native-screens": "link:../"
},
diff --git a/FabricTestExample/src/TestScreenAnimation.tsx b/FabricTestExample/src/TestScreenAnimation.tsx
new file mode 100644
index 0000000000..394adb9354
--- /dev/null
+++ b/FabricTestExample/src/TestScreenAnimation.tsx
@@ -0,0 +1,84 @@
+import React from 'react';
+import { NavigationContainer } from '@react-navigation/native';
+import { View, StyleSheet, Button } from 'react-native';
+import { GestureHandlerRootView } from 'react-native-gesture-handler';
+import {
+ createNativeStackNavigator,
+ NativeStackNavigationProp,
+} from 'react-native-screens/native-stack';
+import { GestureDetectorProvider } from 'react-native-screens/gesture-handler';
+
+type StackParamList = {
+ ScreenA: undefined;
+ ScreenB: undefined;
+ ScreenC: undefined;
+};
+
+interface MainScreenProps {
+ navigation: NativeStackNavigationProp;
+}
+
+const MainScreen = ({ navigation }: MainScreenProps): JSX.Element => (
+
+ {
+ navigation.navigate('ScreenB')
+ }} />
+ navigation.pop()} title="🔙 Back to Examples" />
+
+);
+
+interface ScreenBProps {
+ navigation: NativeStackNavigationProp;
+}
+
+const ScreenB = ({ navigation }: ScreenBProps): JSX.Element => (
+
+ navigation.navigate('ScreenC')} />
+ navigation.goBack()} />
+
+);
+
+interface ScreenCProps {
+ navigation: NativeStackNavigationProp;
+}
+
+const ScreenC = ({ navigation }: ScreenCProps): JSX.Element => (
+
+ navigation.goBack()} />
+
+);
+
+const Stack = createNativeStackNavigator();
+
+const App = (): JSX.Element => (
+
+
+
+
+
+
+
+
+
+
+
+);
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ paddingTop: 10,
+ },
+});
+
+export default App;
diff --git a/FabricTestExample/yarn.lock b/FabricTestExample/yarn.lock
index 5b8db5de1d..e0d36e3d65 100644
--- a/FabricTestExample/yarn.lock
+++ b/FabricTestExample/yarn.lock
@@ -5702,10 +5702,10 @@ react-native-gesture-handler@^2.12.1:
lodash "^4.17.21"
prop-types "^15.7.2"
-react-native-reanimated@^3.6.0:
- version "3.6.0"
- resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-3.6.0.tgz#d2ca5f4c234f592af3d63bc749806e36d6e0a755"
- integrity sha512-eDdhZTRYofrIqFB/Z5xLTWxcB7wDj4ifrNm+gZ2xHSZPjAQ747ukDdH9rglPyPmi+GcmDH7Wff411Xsw5fm45Q==
+react-native-reanimated@3.7.0-nightly-20240109-9e2c33716:
+ version "3.7.0-nightly-20240109-9e2c33716"
+ resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-3.7.0-nightly-20240109-9e2c33716.tgz#ec647feabf1a0ba6673830f6ee2f3d57e0aa63b7"
+ integrity sha512-HMhmzQmAglaHuWwTiMVKx3fKZURi8oVP4u/76KYNBvUDK1Vh+sR54aFQ6d3X877LObw+R9vgr8LmajisY4poVQ==
dependencies:
"@babel/plugin-transform-object-assign" "^7.16.7"
"@babel/preset-typescript" "^7.16.7"
diff --git a/RNScreens.podspec b/RNScreens.podspec
index 45193f784a..612f7c33c0 100644
--- a/RNScreens.podspec
+++ b/RNScreens.podspec
@@ -4,7 +4,7 @@ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
new_arch_enabled = ENV['RCT_NEW_ARCH_ENABLED'] == '1'
platform = new_arch_enabled ? "11.0" : "9.0"
-source_files = new_arch_enabled ? 'ios/**/*.{h,m,mm,cpp}' : "ios/**/*.{h,m,mm}"
+source_files = new_arch_enabled ? 'ios/**/*.{h,m,mm,cpp}' : ["ios/**/*.{h,m,mm}", "cpp/**/*.{cpp,h}"]
folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
@@ -17,7 +17,7 @@ class RNScreensDependencyHelper
end
s.subspec "common" do |ss|
- ss.source_files = "common/cpp/**/*.{cpp,h}"
+ ss.source_files = ["common/cpp/**/*.{cpp,h}", "cpp/**/*.{cpp,h}"]
ss.header_dir = "rnscreens"
ss.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/common/cpp\"" }
end
diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt
new file mode 100644
index 0000000000..5122888626
--- /dev/null
+++ b/android/CMakeLists.txt
@@ -0,0 +1,27 @@
+cmake_minimum_required(VERSION 3.9.0)
+
+project(rnscreens)
+
+add_library(rnscreens
+ SHARED
+ ../cpp/RNScreensTurboModule.cpp
+ ./src/main/cpp/jni-adapter.cpp
+)
+
+include_directories(
+ ../cpp
+)
+
+set_target_properties(rnscreens PROPERTIES
+ CXX_STANDARD 17
+ CXX_STANDARD_REQUIRED ON
+ CXX_EXTENSIONS OFF
+ POSITION_INDEPENDENT_CODE ON
+)
+
+find_package(ReactAndroid REQUIRED CONFIG)
+
+target_link_libraries(rnscreens
+ ReactAndroid::jsi
+ android
+)
diff --git a/android/build.gradle b/android/build.gradle
index 94b5c952e3..c2ecc16236 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -1,4 +1,5 @@
import com.android.Version
+import groovy.json.JsonSlurper
buildscript {
ext {
@@ -73,6 +74,19 @@ android {
ndk {
abiFilters (*reactNativeArchitectures())
}
+ externalNativeBuild {
+ cmake {
+ arguments "-DANDROID_STL=c++_shared"
+ }
+ }
+ }
+ buildFeatures {
+ prefab true
+ }
+ externalNativeBuild {
+ cmake {
+ path "CMakeLists.txt"
+ }
}
lintOptions {
abortOnError false
@@ -86,7 +100,13 @@ android {
// while there are more libraries copied in intermediates folder of the lib build directory, we exclude
// only the ones that make the build fail (ideally we should only include librnscreens_modules but we
// are only allowed to specify exclude patterns)
- exclude "**/libreact_render*.so"
+ excludes = [
+ "META-INF",
+ "META-INF/**",
+ "**/libjsi.so",
+ "**/libc++_shared.so",
+ "**/libreact_render*.so"
+ ]
}
sourceSets.main {
ext.androidResDir = "src/main/res"
@@ -101,7 +121,6 @@ android {
"build/generated/source/codegen/java"
]
}
-
}
res {
if (safeExtGet('compileSdkVersion', rnsDefaultCompileSdkVersion) >= 33) {
diff --git a/android/src/main/cpp/jni-adapter.cpp b/android/src/main/cpp/jni-adapter.cpp
new file mode 100644
index 0000000000..a0dde3c36d
--- /dev/null
+++ b/android/src/main/cpp/jni-adapter.cpp
@@ -0,0 +1,110 @@
+#include
+#include
+#include
+#include
+#include "RNScreensTurboModule.h"
+
+using namespace facebook;
+
+jobject globalThis;
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_com_swmansion_rnscreens_ScreensModule_nativeInstall(JNIEnv *env, jobject thiz, jlong jsiPtr) {
+ auto runtime = reinterpret_cast(jsiPtr);
+ if (!runtime) {
+ return;
+ }
+ jsi::Runtime &rt = *runtime;
+ globalThis = env->NewGlobalRef(thiz);
+ JavaVM* jvm;
+ env->GetJavaVM(&jvm);
+
+ const auto &startTransition = [jvm](int stackTag) -> std::array {
+ JNIEnv* currentEnv;
+ if (jvm->AttachCurrentThread(¤tEnv, nullptr) != JNI_OK) {
+ return {0, 0};
+ }
+ jclass javaClass = currentEnv->GetObjectClass(globalThis);
+ jmethodID methodID = currentEnv->GetMethodID(
+ javaClass,
+ "startTransition",
+ "(Ljava/lang/Integer;)[I"
+ );
+ jclass integerClass = currentEnv->FindClass("java/lang/Integer");
+ jmethodID integerConstructor = currentEnv->GetMethodID(integerClass, "", "(I)V");
+ jobject integerArg = currentEnv->NewObject(integerClass, integerConstructor, stackTag);
+ jintArray resultArray = (jintArray) currentEnv->CallObjectMethod(
+ globalThis,
+ methodID,
+ integerArg
+ );
+ std::array result = {-1, -1};
+ jint* elements = currentEnv->GetIntArrayElements(resultArray, nullptr);
+ if (elements != nullptr) {
+ result[0] = elements[0];
+ result[1] = elements[1];
+ currentEnv->ReleaseIntArrayElements(resultArray, elements, JNI_ABORT);
+ }
+ return result;
+ };
+
+ const auto &updateTransition = [jvm](int stackTag, double progress){
+ JNIEnv* currentEnv;
+ if (jvm->AttachCurrentThread(¤tEnv, nullptr) != JNI_OK) {
+ return;
+ }
+ jclass javaClass = currentEnv->GetObjectClass(globalThis);
+ jmethodID methodID = currentEnv->GetMethodID(
+ javaClass,
+ "updateTransition",
+ "(D)V"
+ );
+ currentEnv->CallVoidMethod(globalThis, methodID, progress);
+ };
+
+ const auto &finishTransition = [jvm](int stackTag, bool canceled){
+ JNIEnv* currentEnv;
+ if (jvm->AttachCurrentThread(¤tEnv, nullptr) != JNI_OK) {
+ return;
+ }
+ jclass javaClass = currentEnv->GetObjectClass(globalThis);
+ jmethodID methodID = currentEnv->GetMethodID(
+ javaClass,
+ "finishTransition",
+ "(Ljava/lang/Integer;Z)V"
+ );
+ jclass integerClass = currentEnv->FindClass("java/lang/Integer");
+ jmethodID integerConstructor = currentEnv->GetMethodID(integerClass, "", "(I)V");
+ jobject integerArg = currentEnv->NewObject(integerClass, integerConstructor, stackTag);
+ currentEnv->CallVoidMethod(globalThis, methodID, integerArg, canceled);
+ };
+
+ const auto &disableSwipeBackForTopScreen = [](int _stackTag){
+ // no implementation for Android
+ };
+
+ auto rnScreensModule = std::make_shared(
+ startTransition,
+ updateTransition,
+ finishTransition,
+ disableSwipeBackForTopScreen
+ );
+ auto rnScreensModuleHostObject = jsi::Object::createFromHostObject(rt, rnScreensModule);
+ rt.global().setProperty(
+ rt,
+ RNScreens::RNScreensTurboModule::MODULE_NAME,
+ std::move(rnScreensModuleHostObject)
+ );
+}
+
+void JNICALL JNI_OnUnload(JavaVM *jvm, void *) {
+ JNIEnv *env;
+ if (jvm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) {
+ return;
+ }
+ if (globalThis != nullptr) {
+ env->DeleteGlobalRef(globalThis);
+ globalThis = nullptr;
+ }
+}
diff --git a/android/src/main/java/com/swmansion/rnscreens/RNScreensPackage.kt b/android/src/main/java/com/swmansion/rnscreens/RNScreensPackage.kt
index 371ed8cfc3..2780dd6bb2 100644
--- a/android/src/main/java/com/swmansion/rnscreens/RNScreensPackage.kt
+++ b/android/src/main/java/com/swmansion/rnscreens/RNScreensPackage.kt
@@ -1,14 +1,19 @@
package com.swmansion.rnscreens
-import com.facebook.react.ReactPackage
+import com.facebook.react.TurboReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.module.annotations.ReactModuleList
+import com.facebook.react.module.model.ReactModuleInfo
+import com.facebook.react.module.model.ReactModuleInfoProvider
import com.facebook.react.uimanager.ViewManager
-class RNScreensPackage : ReactPackage {
- override fun createNativeModules(reactContext: ReactApplicationContext): List =
- emptyList()
-
+@ReactModuleList(
+ nativeModules = [
+ ScreensModule::class
+ ]
+)
+class RNScreensPackage : TurboReactPackage() {
override fun createViewManagers(reactContext: ReactApplicationContext) =
listOf>(
ScreenContainerViewManager(),
@@ -18,4 +23,31 @@ class RNScreensPackage : ReactPackage {
ScreenStackHeaderSubviewManager(),
SearchBarManager()
)
+
+ override fun getModule(
+ s: String,
+ reactApplicationContext: ReactApplicationContext
+ ): NativeModule? {
+ when (s) {
+ ScreensModule.NAME -> return ScreensModule(reactApplicationContext)
+ }
+ return null
+ }
+
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
+ return ReactModuleInfoProvider {
+ val moduleInfos: MutableMap = HashMap()
+ val isTurboModule = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
+ moduleInfos[ScreensModule.NAME] = ReactModuleInfo(
+ ScreensModule.NAME,
+ ScreensModule.NAME,
+ false, // canOverrideExistingModule
+ false, // needsEagerInit
+ true, // hasConstants
+ false, // isCxxModule
+ isTurboModule
+ )
+ moduleInfos
+ }
+ }
}
diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenContainer.kt b/android/src/main/java/com/swmansion/rnscreens/ScreenContainer.kt
index 90b74b31c8..ac45255e86 100644
--- a/android/src/main/java/com/swmansion/rnscreens/ScreenContainer.kt
+++ b/android/src/main/java/com/swmansion/rnscreens/ScreenContainer.kt
@@ -14,7 +14,9 @@ import com.facebook.react.ReactRootView
import com.facebook.react.bridge.ReactContext
import com.facebook.react.modules.core.ChoreographerCompat
import com.facebook.react.modules.core.ReactChoreographer
+import com.facebook.react.uimanager.UIManagerHelper
import com.swmansion.rnscreens.Screen.ActivityState
+import com.swmansion.rnscreens.events.ScreenDismissedEvent
open class ScreenContainer(context: Context?) : ViewGroup(context) {
@JvmField
@@ -200,6 +202,38 @@ open class ScreenContainer(context: Context?) : ViewGroup(context) {
transaction.add(id, fragment)
}
+ fun attachBelowTop() {
+ if (screenWrappers.size < 2) {
+ throw RuntimeException("[RNScreens] Unable to run transition for less than 2 screens.")
+ }
+ val transaction = createTransaction()
+ val top = topScreen as Screen
+ // we have to reattach topScreen so it is on top of the one below
+ detachScreen(transaction, top.fragment as Fragment)
+ attachScreen(transaction, screenWrappers[screenWrappers.size - 2].fragment)
+ attachScreen(transaction, top.fragment as Fragment)
+ transaction.commitNowAllowingStateLoss()
+ }
+
+ fun detachBelowTop() {
+ if (screenWrappers.size < 2) {
+ throw RuntimeException("[RNScreens] Unable to run transition for less than 2 screens.")
+ }
+ val transaction = createTransaction()
+ detachScreen(transaction, screenWrappers[screenWrappers.size - 2].fragment)
+ transaction.commitNowAllowingStateLoss()
+ }
+
+ fun notifyTopDetached() {
+ val top = topScreen as Screen
+ if (context is ReactContext) {
+ val surfaceId = UIManagerHelper.getSurfaceId(context)
+ UIManagerHelper
+ .getEventDispatcherForReactTag(context as ReactContext, top.id)
+ ?.dispatchEvent(ScreenDismissedEvent(surfaceId, top.id))
+ }
+ }
+
private fun detachScreen(transaction: FragmentTransaction, fragment: Fragment) {
transaction.remove(fragment)
}
diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenFragment.kt b/android/src/main/java/com/swmansion/rnscreens/ScreenFragment.kt
index f73ede4247..3915dd6c1e 100644
--- a/android/src/main/java/com/swmansion/rnscreens/ScreenFragment.kt
+++ b/android/src/main/java/com/swmansion/rnscreens/ScreenFragment.kt
@@ -229,12 +229,7 @@ open class ScreenFragment : Fragment, ScreenFragmentWrapper {
if (this is ScreenStackFragment) {
if (transitionProgress != alpha) {
transitionProgress = max(0.0f, min(1.0f, alpha))
- /* We want value of 0 and 1 to be always dispatched so we base coalescing key on the progress:
- - progress is 0 -> key 1
- - progress is 1 -> key 2
- - progress is between 0 and 1 -> key 3
- */
- val coalescingKey = (if (transitionProgress == 0.0f) 1 else if (transitionProgress == 1.0f) 2 else 3).toShort()
+ val coalescingKey = getCoalescingKey(transitionProgress)
val container: ScreenContainer? = screen.container
val goingForward = if (container is ScreenStack) container.goingForward else false
val screenContext = screen.context as ReactContext
@@ -326,5 +321,14 @@ open class ScreenFragment : Fragment, ScreenFragmentWrapper {
view.visibility = View.VISIBLE
return view
}
+
+ fun getCoalescingKey(progress: Float): Short {
+ /* We want value of 0 and 1 to be always dispatched so we base coalescing key on the progress:
+ - progress is 0 -> key 1
+ - progress is 1 -> key 2
+ - progress is between 0 and 1 -> key 3
+ */
+ return (if (progress == 0.0f) 1 else if (progress == 1.0f) 2 else 3).toShort()
+ }
}
}
diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenStack.kt b/android/src/main/java/com/swmansion/rnscreens/ScreenStack.kt
index da2d831df6..eff7680d76 100644
--- a/android/src/main/java/com/swmansion/rnscreens/ScreenStack.kt
+++ b/android/src/main/java/com/swmansion/rnscreens/ScreenStack.kt
@@ -31,6 +31,9 @@ class ScreenStack(context: Context?) : ScreenContainer(context) {
override val topScreen: Screen?
get() = topScreenWrapper?.screen
+ val fragments: ArrayList
+ get() = stack
+
val rootScreen: Screen
get() {
for (i in 0 until screenCount) {
diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreensModule.kt b/android/src/main/java/com/swmansion/rnscreens/ScreensModule.kt
new file mode 100644
index 0000000000..a3490fc177
--- /dev/null
+++ b/android/src/main/java/com/swmansion/rnscreens/ScreensModule.kt
@@ -0,0 +1,105 @@
+package com.swmansion.rnscreens
+
+import android.util.Log
+import com.facebook.proguard.annotations.DoNotStrip
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.UiThreadUtil
+import com.facebook.react.module.annotations.ReactModule
+import com.facebook.react.uimanager.UIManagerHelper
+import com.swmansion.rnscreens.events.ScreenTransitionProgressEvent
+import java.util.concurrent.atomic.AtomicBoolean
+
+@ReactModule(name = ScreensModule.NAME)
+class ScreensModule(private val reactContext: ReactApplicationContext)
+ : NativeScreensModuleSpec(reactContext)
+{
+ private var topScreenId: Int = -1
+ private val isActiveTransition = AtomicBoolean(false)
+
+ init {
+ try {
+ System.loadLibrary("rnscreens")
+ val jsContext = reactApplicationContext.javaScriptContextHolder
+ if (jsContext != null) {
+ nativeInstall(jsContext.get())
+ } else {
+ Log.e("[RNScreens]", "Could not install JSI bindings.")
+ }
+ } catch (exception: Exception) {
+ Log.w("[RNScreens]", "Could not load RNScreens module.")
+ }
+ }
+
+ private external fun nativeInstall(jsiPtr: Long)
+
+ override fun getName(): String = NAME
+
+ @DoNotStrip
+ private fun startTransition(reactTag: Int?): IntArray {
+ UiThreadUtil.assertOnUiThread()
+ if (isActiveTransition.get() || reactTag == null) {
+ return intArrayOf(-1, -1)
+ }
+ topScreenId = -1
+ val result = intArrayOf(-1, -1)
+ val uiManager = UIManagerHelper.getUIManagerForReactTag(reactContext, reactTag)
+ val stack = uiManager?.resolveView(reactTag)
+ if (stack is ScreenStack) {
+ val fragments = stack.fragments
+ val screensCount = fragments.size
+ if (screensCount > 1) {
+ isActiveTransition.set(true)
+ stack.attachBelowTop()
+ topScreenId = fragments[screensCount - 1].screen.id
+ result[0] = topScreenId
+ result[1] = fragments[screensCount - 2].screen.id
+ }
+ }
+ return result
+ }
+
+ @DoNotStrip
+ private fun updateTransition(progress: Double) {
+ UiThreadUtil.assertOnUiThread()
+ if (topScreenId == -1) {
+ return
+ }
+ val progressFloat = progress.toFloat();
+ val coalescingKey = ScreenFragment.getCoalescingKey(progressFloat)
+ UIManagerHelper
+ .getEventDispatcherForReactTag(reactContext, topScreenId)
+ ?.dispatchEvent(
+ ScreenTransitionProgressEvent(
+ UIManagerHelper.getSurfaceId(reactContext),
+ topScreenId, progressFloat, true, true, coalescingKey
+ )
+ )
+ }
+
+ @DoNotStrip
+ private fun finishTransition(reactTag: Int?, canceled: Boolean) {
+ UiThreadUtil.assertOnUiThread()
+ if (!isActiveTransition.get() || reactTag == null) {
+ Log.e(
+ "[RNScreens]",
+ "Unable to call `finishTransition` method before transition start."
+ )
+ return
+ }
+ val uiManager = UIManagerHelper.getUIManagerForReactTag(reactContext, reactTag)
+ val stack = uiManager?.resolveView(reactTag)
+ if (stack is ScreenStack) {
+ if (canceled) {
+ stack.detachBelowTop()
+ } else {
+ stack.notifyTopDetached()
+ }
+ isActiveTransition.set(false)
+ }
+ topScreenId = -1
+ }
+
+ companion object {
+ const val NAME = "RNSModule"
+ }
+}
diff --git a/android/src/paper/java/com/swmansion/rnscreens/NativeScreensModuleSpec.java b/android/src/paper/java/com/swmansion/rnscreens/NativeScreensModuleSpec.java
new file mode 100644
index 0000000000..d7f97b2276
--- /dev/null
+++ b/android/src/paper/java/com/swmansion/rnscreens/NativeScreensModuleSpec.java
@@ -0,0 +1,32 @@
+
+/**
+ * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
+ *
+ * Do not edit this file as changes may cause incorrect behavior and will be lost
+ * once the code is regenerated.
+ *
+ * @generated by codegen project: GenerateModuleJavaSpec.js
+ *
+ * @nolint
+ */
+
+package com.swmansion.rnscreens;
+
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+import com.facebook.react.bridge.ReactModuleWithSpec;
+import com.facebook.react.turbomodule.core.interfaces.TurboModule;
+import javax.annotation.Nonnull;
+
+public abstract class NativeScreensModuleSpec extends ReactContextBaseJavaModule implements ReactModuleWithSpec, TurboModule {
+ public static final String NAME = "RNSModule";
+
+ public NativeScreensModuleSpec(ReactApplicationContext reactContext) {
+ super(reactContext);
+ }
+
+ @Override
+ public @Nonnull String getName() {
+ return NAME;
+ }
+}
diff --git a/cpp/RNScreensTurboModule.cpp b/cpp/RNScreensTurboModule.cpp
new file mode 100644
index 0000000000..8187e5e467
--- /dev/null
+++ b/cpp/RNScreensTurboModule.cpp
@@ -0,0 +1,107 @@
+#include "RNScreensTurboModule.h"
+
+using namespace facebook;
+
+namespace RNScreens {
+
+const char RNScreensTurboModule::MODULE_NAME[] = "RNScreensTurboModule";
+
+std::function(int)> RNScreensTurboModule::startTransition_;
+std::function RNScreensTurboModule::updateTransition_;
+std::function RNScreensTurboModule::finishTransition_;
+std::function RNScreensTurboModule::disableSwipeBackForTopScreen_;
+
+RNScreensTurboModule::RNScreensTurboModule(
+ std::function(int)> startTransition,
+ std::function updateTransition,
+ std::function finishTransition,
+ std::function disableSwipeBackForTopScreen
+) {
+ startTransition_ = startTransition;
+ updateTransition_ = updateTransition;
+ finishTransition_ = finishTransition;
+ disableSwipeBackForTopScreen_ = disableSwipeBackForTopScreen;
+}
+
+RNScreensTurboModule::~RNScreensTurboModule() {};
+
+jsi::Value RNScreensTurboModule::get(jsi::Runtime& rt, const jsi::PropNameID& name) {
+ if (name.utf8(rt) == "startTransition") {
+ return jsi::Function::createFromHostFunction(rt, name, 1, startTransition);
+ }
+ else if (name.utf8(rt) == "updateTransition") {
+ return jsi::Function::createFromHostFunction(rt, name, 2, updateTransition);
+ }
+ else if (name.utf8(rt) == "finishTransition") {
+ return jsi::Function::createFromHostFunction(rt, name, 2, finishTransition);
+ }
+ else if (name.utf8(rt) == "disableSwipeBackForTopScreen") {
+ return jsi::Function::createFromHostFunction(rt, name, 1, disableSwipeBackForTopScreen);
+ }
+ return jsi::Value::undefined();
+}
+
+void RNScreensTurboModule::set(jsi::Runtime&, const jsi::PropNameID&, const jsi::Value&) {};
+
+std::vector RNScreensTurboModule::getPropertyNames(jsi::Runtime& rt) {
+ std::vector properties;
+ properties.push_back(jsi::PropNameID::forUtf8(rt, "startTransition"));
+ properties.push_back(jsi::PropNameID::forUtf8(rt, "updateTransition"));
+ properties.push_back(jsi::PropNameID::forUtf8(rt, "finishTransition"));
+ properties.push_back(jsi::PropNameID::forUtf8(rt, "disableSwipeBackForTopScreen"));
+ return properties;
+}
+
+JSI_HOST_FUNCTION(RNScreensTurboModule::startTransition) {
+ if (count < 1) {
+ throw jsi::JSError(rt, "[RNScreens] `startTransition` method requires 1 argument.");
+ }
+ int stackTag = arguments[0].asNumber();
+ auto screenTags = startTransition_(stackTag);
+ jsi::Object screenTagsObject(rt);
+ jsi::Value topScreenTag, belowTopScreenTag, canStartTransition;
+ if (screenTags[0] > -1) {
+ topScreenTag = jsi::Value(screenTags[0]);
+ belowTopScreenTag = jsi::Value(screenTags[1]);
+ canStartTransition = jsi::Value(true);
+ } else {
+ topScreenTag = jsi::Value(-1);
+ belowTopScreenTag = jsi::Value(-1);
+ canStartTransition = jsi::Value(false);
+ }
+ screenTagsObject.setProperty(rt, "topScreenTag", topScreenTag);
+ screenTagsObject.setProperty(rt, "belowTopScreenTag", belowTopScreenTag);
+ screenTagsObject.setProperty(rt, "canStartTransition", canStartTransition);
+ return screenTagsObject;
+}
+
+JSI_HOST_FUNCTION(RNScreensTurboModule::updateTransition) {
+ if (count < 2) {
+ throw jsi::JSError(rt, "[RNScreens] `updateTransition` requires 2 arguments.");
+ }
+ int stackTag = arguments[0].asNumber();
+ double progress = arguments[1].asNumber();
+ updateTransition_(stackTag, progress);
+ return jsi::Value::undefined();
+}
+
+JSI_HOST_FUNCTION(RNScreensTurboModule::finishTransition) {
+ if (count < 2) {
+ throw jsi::JSError(rt, "[RNScreens] `finishTransition` requires 2 arguments.");
+ }
+ int stackTag = arguments[0].asNumber();
+ bool canceled = arguments[1].getBool();
+ finishTransition_(stackTag, canceled);
+ return jsi::Value::undefined();
+}
+
+JSI_HOST_FUNCTION(RNScreensTurboModule::disableSwipeBackForTopScreen) {
+if (count < 1) {
+ throw jsi::JSError(rt, "[RNScreens] `startTransition` method requires 1 argument.");
+ }
+ int stackTag = arguments[0].asNumber();
+ disableSwipeBackForTopScreen_(stackTag);
+ return jsi::Value::undefined();
+}
+
+}
diff --git a/cpp/RNScreensTurboModule.h b/cpp/RNScreensTurboModule.h
new file mode 100644
index 0000000000..2d9e88d532
--- /dev/null
+++ b/cpp/RNScreensTurboModule.h
@@ -0,0 +1,43 @@
+#include
+#include
+
+#define JSI_HOST_FUNCTION(NAME) \
+ jsi::Value NAME( \
+ jsi::Runtime &rt, \
+ const jsi::Value &thisValue, \
+ const jsi::Value *arguments, \
+ size_t count \
+ )
+
+using namespace facebook;
+
+namespace RNScreens {
+
+class RNScreensTurboModule : public jsi::HostObject {
+
+ static std::function(int)> startTransition_;
+ static std::function updateTransition_;
+ static std::function finishTransition_;
+ static std::function disableSwipeBackForTopScreen_;
+
+public:
+ static const char MODULE_NAME[];
+
+ RNScreensTurboModule(
+ std::function(int)> startTransition,
+ std::function updateTransition,
+ std::function finishTransition,
+ std::function disableSwipeBackForTopScreen
+ );
+ ~RNScreensTurboModule() override;
+ jsi::Value get(jsi::Runtime& rt, const jsi::PropNameID& name) override;
+ void set(jsi::Runtime&, const jsi::PropNameID&, const jsi::Value&) override;
+ std::vector getPropertyNames(jsi::Runtime& rt) override;
+ static JSI_HOST_FUNCTION(startTransition);
+ static JSI_HOST_FUNCTION(updateTransition);
+ static JSI_HOST_FUNCTION(finishTransition);
+ static JSI_HOST_FUNCTION(disableSwipeBackForTopScreen);
+
+};
+
+}
diff --git a/gesture-handler/package.json b/gesture-handler/package.json
new file mode 100644
index 0000000000..282b21bf8d
--- /dev/null
+++ b/gesture-handler/package.json
@@ -0,0 +1,6 @@
+{
+ "main": "../lib/commonjs/gesture-handler/index",
+ "module": "../lib/module/gesture-handler/index",
+ "react-native": "../src/gesture-handler/index",
+ "types": "../lib/typescript/gesture-handler/index"
+}
diff --git a/ios/RNSModule.h b/ios/RNSModule.h
new file mode 100644
index 0000000000..bdd4fe30f9
--- /dev/null
+++ b/ios/RNSModule.h
@@ -0,0 +1,19 @@
+
+#ifdef RCT_NEW_ARCH_ENABLED
+#import
+#else
+#import
+#endif
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface RNSModule : NSObject
+#ifdef RCT_NEW_ARCH_ENABLED
+
+#else
+
+#endif
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/ios/RNSModule.mm b/ios/RNSModule.mm
new file mode 100644
index 0000000000..53a2dd3a8d
--- /dev/null
+++ b/ios/RNSModule.mm
@@ -0,0 +1,174 @@
+#import "RNSModule.h"
+#import
+#import
+#import
+#import
+#import
+#include
+#import "RNSScreenStack.h"
+#import "RNScreensTurboModule.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation RNSModule {
+ std::atomic isActiveTransition;
+}
+
+RCT_EXPORT_MODULE()
+
+#ifdef RCT_NEW_ARCH_ENABLED
+@synthesize viewRegistry_DEPRECATED = _viewRegistry_DEPRECATED;
+#endif // RCT_NEW_ARCH_ENABLED
+@synthesize bridge = _bridge;
+
+- (dispatch_queue_t)methodQueue
+{
+ // It seems that due to how UIBlocks work with uiManager, we need to call the methods there
+ // for the blocks to be dispatched before the batch is completed
+ return dispatch_get_main_queue();
+}
+
+- (NSDictionary *)constantsToExport
+{
+ [self installHostObject];
+ return @{};
+}
+
++ (BOOL)requiresMainQueueSetup
+{
+ return NO;
+}
+
+- (RNSScreenStackView *)getScreenStackView:(NSNumber *)reactTag
+{
+ RCTAssertMainQueue();
+ RNSScreenStackView *view;
+#ifdef RCT_NEW_ARCH_ENABLED
+ view = (RNSScreenStackView *)[self.viewRegistry_DEPRECATED viewForReactTag:reactTag];
+#else
+ view = (RNSScreenStackView *)[self.bridge.uiManager viewForReactTag:reactTag];
+#endif // RCT_NEW_ARCH_ENABLED
+ return view;
+}
+
+- (NSArray *)startTransition:(NSNumber *)stackTag
+{
+ RCTAssertMainQueue();
+ RNSScreenStackView *stackView = [self getStackView:stackTag];
+ if (stackView == nil || isActiveTransition) {
+ return @[ @(-1), @(-1) ];
+ }
+ NSArray *screenTags = @[ @(-1), @(-1) ];
+ auto screens = stackView.reactViewController.childViewControllers;
+ unsigned long screenCount = [screens count];
+ if (screenCount > 1) {
+ UIView *topScreen = screens[screenCount - 1].view;
+ UIView *belowTopScreen = screens[screenCount - 2].view;
+ belowTopScreen.transform = CGAffineTransformMake(1, 0, 0, 1, 0, 0);
+ isActiveTransition = true;
+#ifdef RCT_NEW_ARCH_ENABLED
+ screenTags = @[ @(topScreen.tag), @(belowTopScreen.tag) ];
+#else
+ screenTags = @[ topScreen.reactTag, belowTopScreen.reactTag ];
+#endif // RCT_NEW_ARCH_ENABLED
+ [stackView startScreenTransition];
+ }
+ return screenTags;
+}
+
+- (bool)updateTransition:(NSNumber *)stackTag progress:(double)progress
+{
+ RCTAssertMainQueue();
+ RNSScreenStackView *stackView = [self getStackView:stackTag];
+ if (stackView == nil || !isActiveTransition) {
+ return false;
+ }
+ [stackView updateScreenTransition:progress];
+ return true;
+}
+
+- (bool)finishTransition:(NSNumber *)stackTag canceled:(bool)canceled
+{
+ RCTAssertMainQueue();
+ RNSScreenStackView *stackView = [self getStackView:stackTag];
+ if (stackView == nil || !isActiveTransition) {
+ return false;
+ }
+ [stackView finishScreenTransition:canceled];
+ if (!canceled) {
+ stackView.disableSwipeBack = NO;
+ }
+ isActiveTransition = false;
+ return true;
+}
+
+- (void)disableSwipeBackForTopScreen:(NSNumber *)stackTag
+{
+ RCTAssertMainQueue();
+ RNSScreenStackView *stackView = [self getStackView:stackTag];
+ if (stackView == nil) {
+ return;
+ }
+ stackView.disableSwipeBack = YES;
+}
+
+- (RNSScreenStackView *)getStackView:(NSNumber *)stackTag
+{
+ RNSScreenStackView *view = [self getScreenStackView:stackTag];
+ if (![view isKindOfClass:[RNSScreenStackView class]]) {
+ RCTLogError(@"Invalid view type, expecting RNSScreenStackView, got: %@", view);
+ return nil;
+ }
+ return view;
+}
+
+#ifdef RCT_NEW_ARCH_ENABLED
+- (std::shared_ptr)getTurboModule:
+ (const facebook::react::ObjCTurboModule::InitParams &)params
+{
+ [self installHostObject];
+ return std::make_shared(params);
+}
+#endif
+
+- (void)installHostObject
+{
+ /*
+ installHostObject method is called from constantsToExport and getTurboModule,
+ because depending on the selected architecture, only one method will be called.
+ For `Paper`, it will be constantsToExport, and for `Fabric`, it will be getTurboModule.
+*/
+ RCTBridge *bridge = [RCTBridge currentBridge];
+ RCTCxxBridge *cxxBridge = (RCTCxxBridge *)bridge;
+ if (cxxBridge != nil) {
+ auto jsiRuntime = (jsi::Runtime *)cxxBridge.runtime;
+ if (jsiRuntime != nil) {
+ auto &runtime = *jsiRuntime;
+ __weak auto weakSelf = self;
+
+ const auto &startTransition = [weakSelf](int stackTag) -> std::array {
+ auto screensTags = [weakSelf startTransition:@(stackTag)];
+ return {[screensTags[0] intValue], [screensTags[1] intValue]};
+ };
+ const auto &updateTransition = [weakSelf](int stackTag, double progress) {
+ [weakSelf updateTransition:@(stackTag) progress:progress];
+ };
+ const auto &finishTransition = [weakSelf](int stackTag, bool canceled) {
+ [weakSelf finishTransition:@(stackTag) canceled:canceled];
+ };
+ const auto &disableSwipeBackForTopScreen = [weakSelf](int stackTag) {
+ [weakSelf disableSwipeBackForTopScreen:@(stackTag)];
+ };
+
+ auto rnScreensModule = std::make_shared(
+ startTransition, updateTransition, finishTransition, disableSwipeBackForTopScreen);
+ auto rnScreensModuleHostObject = jsi::Object::createFromHostObject(runtime, rnScreensModule);
+ runtime.global().setProperty(
+ runtime, RNScreens::RNScreensTurboModule::MODULE_NAME, std::move(rnScreensModuleHostObject));
+ }
+ }
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/ios/RNSScreenStack.h b/ios/RNSScreenStack.h
index 0f5fd1b05d..516fc49d99 100644
--- a/ios/RNSScreenStack.h
+++ b/ios/RNSScreenStack.h
@@ -22,6 +22,12 @@ NS_ASSUME_NONNULL_BEGIN
- (void)markChildUpdated;
- (void)didUpdateChildren;
+- (void)startScreenTransition;
+- (void)updateScreenTransition:(double)progress;
+- (void)finishScreenTransition:(BOOL)canceled;
+
+@property (nonatomic) BOOL customAnimation;
+@property (nonatomic) BOOL disableSwipeBack;
#ifdef RCT_NEW_ARCH_ENABLED
#else
diff --git a/ios/RNSScreenStack.mm b/ios/RNSScreenStack.mm
index 71dbaee5fc..3d678a04e9 100644
--- a/ios/RNSScreenStack.mm
+++ b/ios/RNSScreenStack.mm
@@ -681,7 +681,7 @@ - (void)dismissOnReload
// Also, we need to return the animator when full width swiping even if the animation is not custom,
// otherwise the screen will be just popped immediately due to no animation
((operation == UINavigationControllerOperationPop && shouldCancelDismiss) || _isFullWidthSwiping ||
- [RNSScreenStackAnimator isCustomAnimation:screen.stackAnimation])) {
+ [RNSScreenStackAnimator isCustomAnimation:screen.stackAnimation] || _customAnimation)) {
return [[RNSScreenStackAnimator alloc] initWithOperation:operation];
}
return nil;
@@ -710,6 +710,9 @@ - (void)cancelTouchesInParent
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
+ if (_disableSwipeBack) {
+ return NO;
+ }
RNSScreenView *topScreen = _reactSubviews.lastObject;
#if TARGET_OS_TV
@@ -1033,6 +1036,33 @@ - (void)didUpdateReactSubviews
});
}
+- (void)startScreenTransition
+{
+ if (_interactionController == nil) {
+ _customAnimation = YES;
+ _interactionController = [UIPercentDrivenInteractiveTransition new];
+ [_controller popViewControllerAnimated:YES];
+ }
+}
+
+- (void)updateScreenTransition:(double)progress
+{
+ [_interactionController updateInteractiveTransition:progress];
+}
+
+- (void)finishScreenTransition:(BOOL)canceled
+{
+ _customAnimation = NO;
+ if (canceled) {
+ [_interactionController updateInteractiveTransition:0.0];
+ [_interactionController cancelInteractiveTransition];
+ } else {
+ [_interactionController updateInteractiveTransition:1.0];
+ [_interactionController finishInteractiveTransition];
+ }
+ _interactionController = nil;
+}
+
#ifdef RCT_NEW_ARCH_ENABLED
#pragma mark - Fabric specific
diff --git a/ios/RNSScreenStackAnimator.mm b/ios/RNSScreenStackAnimator.mm
index 6fc0542de9..22ed70b919 100644
--- a/ios/RNSScreenStackAnimator.mm
+++ b/ios/RNSScreenStackAnimator.mm
@@ -64,7 +64,10 @@ - (void)animateTransition:(id)transitionCo
}
if (screen != nil) {
- if (screen.fullScreenSwipeEnabled && transitionContext.isInteractive) {
+ if ([screen.reactSuperview isKindOfClass:[RNSScreenStackView class]] &&
+ ((RNSScreenStackView *)(screen.reactSuperview)).customAnimation) {
+ [self animateWithNoAnimation:transitionContext toVC:toViewController fromVC:fromViewController];
+ } else if (screen.fullScreenSwipeEnabled && transitionContext.isInteractive) {
// we are swiping with full width gesture
if (screen.customAnimationOnSwipe) {
[self animateTransitionWithStackAnimation:screen.stackAnimation
@@ -290,6 +293,30 @@ - (void)animateFadeFromBottomWithTransitionContext:(id)transitionContext
+ toVC:(UIViewController *)toViewController
+ fromVC:(UIViewController *)fromViewController
+{
+ if (_operation == UINavigationControllerOperationPush) {
+ [[transitionContext containerView] addSubview:toViewController.view];
+ [UIView animateWithDuration:[self transitionDuration:transitionContext]
+ animations:^{
+ }
+ completion:^(BOOL finished) {
+ [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
+ }];
+ } else if (_operation == UINavigationControllerOperationPop) {
+ [[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view];
+
+ [UIView animateWithDuration:[self transitionDuration:transitionContext]
+ animations:^{
+ }
+ completion:^(BOOL finished) {
+ [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
+ }];
+ }
+}
+
+ (BOOL)isCustomAnimation:(RNSScreenStackAnimation)animation
{
return (animation != RNSScreenStackAnimationFlip && animation != RNSScreenStackAnimationDefault);
diff --git a/package.json b/package.json
index a871456d1b..3f2da19e7f 100644
--- a/package.json
+++ b/package.json
@@ -26,15 +26,19 @@
"common/",
"lib/",
"native-stack/",
+ "gesture-handler/",
"reanimated/",
"android/src/main/AndroidManifest.xml",
"android/src/main/java/",
+ "android/src/main/cpp/",
"android/src/main/jni/",
"android/src/main/res",
"android/src/fabric/",
"android/src/paper/",
"android/build.gradle",
+ "android/CMakeLists.txt",
"ios/",
+ "cpp/",
"windows/",
"RNScreens.podspec",
"react-native.config.js",
@@ -68,6 +72,9 @@
"@babel/core": "^7.20.0",
"@babel/eslint-parser": "7.22.15",
"@react-native-community/bob": "^0.17.1",
+ "@react-native-community/cli": "^11.3.6",
+ "@react-native-community/cli-platform-android": "^11.3.6",
+ "@react-native-community/cli-platform-ios": "^11.3.6",
"@react-navigation/native": "^5.8.0",
"@react-navigation/stack": "^5.10.0",
"@types/jest": "^29.3.1",
@@ -75,9 +82,6 @@
"@types/react-test-renderer": "^18.0.0",
"@typescript-eslint/eslint-plugin": "^6.5.0",
"@typescript-eslint/parser": "^6.5.0",
- "@react-native-community/cli": "^11.3.6",
- "@react-native-community/cli-platform-android": "^11.3.6",
- "@react-native-community/cli-platform-ios": "^11.3.6",
"babel-jest": "^29.6.4",
"clang-format": "^1.8.0",
"eslint": "^8.19.0",
@@ -98,7 +102,8 @@
"react": "18.2.0",
"react-dom": "^18.2.0",
"react-native": "0.72.4",
- "react-native-reanimated": "^2.2.0",
+ "react-native-gesture-handler": "^2.13.3",
+ "react-native-reanimated": "3.7.0-nightly-20240109-9e2c33716",
"react-native-safe-area-context": "^4.8.1",
"react-native-windows": "^0.64.8",
"react-test-renderer": "^18.2.0",
@@ -137,7 +142,7 @@
],
"codegenConfig": {
"name": "rnscreens",
- "type": "components",
+ "type": "all",
"jsSrcsDir": "./src/fabric",
"android": {
"javaPackageName": "com.swmansion.rnscreens"
diff --git a/src/fabric/NativeScreensModule.ts b/src/fabric/NativeScreensModule.ts
new file mode 100644
index 0000000000..23c2bc767c
--- /dev/null
+++ b/src/fabric/NativeScreensModule.ts
@@ -0,0 +1,7 @@
+/* eslint-disable @typescript-eslint/ban-types */
+import type { TurboModule } from 'react-native';
+import { TurboModuleRegistry } from 'react-native';
+
+export interface Spec extends TurboModule {}
+
+export default TurboModuleRegistry.get('RNSModule');
diff --git a/src/fabric/NativeScreensModule.web.ts b/src/fabric/NativeScreensModule.web.ts
new file mode 100644
index 0000000000..ff8b4c5632
--- /dev/null
+++ b/src/fabric/NativeScreensModule.web.ts
@@ -0,0 +1 @@
+export default {};
diff --git a/src/gesture-handler/GestureDetectorProvider.tsx b/src/gesture-handler/GestureDetectorProvider.tsx
new file mode 100644
index 0000000000..d1be0e60a8
--- /dev/null
+++ b/src/gesture-handler/GestureDetectorProvider.tsx
@@ -0,0 +1,16 @@
+import React from 'react';
+import { GHContext } from '../index';
+import ScreenGestureDetector from './ScreenGestureDetector';
+import type { GestureProviderProps } from '../native-stack/types';
+
+function GHWrapper(props: GestureProviderProps) {
+ return ;
+}
+
+export default function GestureDetectorProvider(props: {
+ children: React.ReactNode;
+}) {
+ return (
+ {props.children}
+ );
+}
diff --git a/src/gesture-handler/RNScreensTurboModule.ts b/src/gesture-handler/RNScreensTurboModule.ts
new file mode 100644
index 0000000000..d096073734
--- /dev/null
+++ b/src/gesture-handler/RNScreensTurboModule.ts
@@ -0,0 +1,13 @@
+type RNScreensTurboModuleType = {
+ startTransition: (stackTag: number) => {
+ topScreenTag: number;
+ belowTopScreenTag: number;
+ canStartTransition: boolean;
+ };
+ updateTransition: (stackTag: number, progress: number) => void;
+ finishTransition: (stackTag: number, isCanceled: boolean) => void;
+ disableSwipeBackForTopScreen: (stackTag: number) => void;
+};
+
+export const RNScreensTurboModule: RNScreensTurboModuleType = (global as any)
+ .RNScreensTurboModule;
diff --git a/src/gesture-handler/ScreenGestureDetector.tsx b/src/gesture-handler/ScreenGestureDetector.tsx
new file mode 100644
index 0000000000..d42795d225
--- /dev/null
+++ b/src/gesture-handler/ScreenGestureDetector.tsx
@@ -0,0 +1,239 @@
+import React, { useEffect } from 'react';
+import { Dimensions, Platform, findNodeHandle } from 'react-native';
+import {
+ GestureDetector,
+ Gesture,
+ PanGestureHandlerEventPayload,
+ GestureUpdateEvent,
+} from 'react-native-gesture-handler';
+import {
+ useSharedValue,
+ measure,
+ startScreenTransition,
+ finishScreenTransition,
+ 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';
+import {
+ checkBoundaries,
+ checkIfTransitionCancelled,
+ getAnimationForTransition,
+} from './constraints';
+
+const EmptyGestureHandler = Gesture.Tap();
+
+const ScreenGestureDetector = ({
+ children,
+ gestureDetectorBridge,
+ goBackGesture,
+ screenEdgeGesture,
+ transitionAnimation: customTransitionAnimation,
+ screensRefs,
+ currentRouteKey,
+}: GestureProviderProps) => {
+ const sharedEvent = useSharedValue(DefaultEvent);
+ const startingGesturePosition = useSharedValue(DefaultEvent);
+ const canPerformUpdates = makeMutable(false);
+ const transitionAnimation = getAnimationForTransition(
+ goBackGesture,
+ customTransitionAnimation
+ );
+ const screenTransitionConfig = makeMutable({
+ stackTag: -1,
+ belowTopScreenId: -1,
+ topScreenId: -1,
+ sharedEvent,
+ startingGesturePosition,
+ screenTransition: transitionAnimation,
+ isTransitionCanceled: false,
+ goBackGesture: goBackGesture ?? 'swipeRight',
+ screenDimensions: DefaultScreenDimensions,
+ onFinishAnimation: () => {
+ 'worklet';
+ },
+ });
+ const stackTag = makeMutable(-1);
+ const screenTagToNodeWrapperUI = makeMutable>({});
+ const IS_FABRIC = isFabric();
+
+ gestureDetectorBridge.current.stackUseEffectCallback = stackRef => {
+ if (!goBackGesture) {
+ return;
+ }
+ stackTag.value = findNodeHandle(stackRef.current as any) as number;
+ if (Platform.OS === 'ios') {
+ runOnUI(() => {
+ RNScreensTurboModule.disableSwipeBackForTopScreen(stackTag.value);
+ })();
+ }
+ };
+
+ useEffect(() => {
+ if (!IS_FABRIC || !goBackGesture) {
+ return;
+ }
+ const screenTagToNodeWrapper: Record> = {};
+ for (const key in screensRefs.current) {
+ const screenRef = screensRefs.current[key];
+ const screenData = getShadowNodeWrapperAndTagFromRef(screenRef.current);
+ if (screenData.tag && screenData.shadowNodeWrapper) {
+ screenTagToNodeWrapper[screenData.tag] = screenData.shadowNodeWrapper;
+ } else {
+ console.warn('[RNScreens] Failed to find tag for screen.');
+ }
+ }
+ screenTagToNodeWrapperUI.value = screenTagToNodeWrapper;
+ }, [currentRouteKey]);
+
+ function computeProgress(
+ event: GestureUpdateEvent
+ ) {
+ 'worklet';
+ let progress = 0;
+ const screenDimensions = screenTransitionConfig.value.screenDimensions;
+ const startingPosition = startingGesturePosition.value;
+ if (goBackGesture === 'swipeRight') {
+ progress =
+ event.translationX /
+ (screenDimensions.width - startingPosition.absoluteX);
+ } else if (goBackGesture === 'swipeLeft') {
+ progress = (-1 * event.translationX) / startingPosition.absoluteX;
+ } else if (goBackGesture === 'swipeDown') {
+ progress =
+ (-1 * event.translationY) /
+ (screenDimensions.height - startingPosition.absoluteY);
+ } else if (goBackGesture === 'swipeUp') {
+ progress = event.translationY / startingPosition.absoluteY;
+ } else if (goBackGesture === 'horizontalSwipe') {
+ progress = Math.abs(event.translationX / screenDimensions.width / 2);
+ } else if (goBackGesture === 'verticalSwipe') {
+ progress = Math.abs(event.translationY / screenDimensions.height / 2);
+ } else if (goBackGesture === 'twoDimensionalSwipe') {
+ const progressX = Math.abs(
+ event.translationX / screenDimensions.width / 2
+ );
+ const progressY = Math.abs(
+ event.translationY / screenDimensions.height / 2
+ );
+ progress = Math.max(progressX, progressY);
+ }
+ return progress;
+ }
+
+ function onStart(event: GestureUpdateEvent) {
+ 'worklet';
+ sharedEvent.value = event;
+ const transitionConfig = screenTransitionConfig.value;
+ const transitionData = RNScreensTurboModule.startTransition(stackTag.value);
+ if (transitionData.canStartTransition === false) {
+ canPerformUpdates.value = false;
+ return;
+ }
+
+ if (IS_FABRIC) {
+ transitionConfig.topScreenId =
+ screenTagToNodeWrapperUI.value[transitionData.topScreenTag];
+ transitionConfig.belowTopScreenId =
+ screenTagToNodeWrapperUI.value[transitionData.belowTopScreenTag];
+ } else {
+ transitionConfig.topScreenId = transitionData.topScreenTag;
+ transitionConfig.belowTopScreenId = transitionData.belowTopScreenTag;
+ }
+
+ transitionConfig.stackTag = stackTag.value;
+ startingGesturePosition.value = event;
+ const animatedRefMock = () => {
+ return screenTransitionConfig.value.topScreenId;
+ };
+ const screenSize = measure(animatedRefMock as any);
+ if (screenSize == null) {
+ throw new Error('[RNScreens] Failed to measure screen.');
+ }
+ if (screenSize == null) {
+ canPerformUpdates.value = false;
+ RNScreensTurboModule.finishTransition(stackTag.value, true);
+ return;
+ }
+ transitionConfig.screenDimensions = screenSize;
+ startScreenTransition(transitionConfig);
+ canPerformUpdates.value = true;
+ }
+
+ function onUpdate(event: GestureUpdateEvent) {
+ 'worklet';
+ if (!canPerformUpdates.value) {
+ return;
+ }
+ checkBoundaries(goBackGesture, event);
+ const progress = computeProgress(event);
+ sharedEvent.value = event;
+ const stackTag = screenTransitionConfig.value.stackTag;
+ RNScreensTurboModule.updateTransition(stackTag, progress);
+ }
+
+ function onEnd(event: GestureUpdateEvent) {
+ 'worklet';
+ if (!canPerformUpdates.value) {
+ return;
+ }
+
+ const velocityFactor = 0.3;
+ const screenSize = screenTransitionConfig.value.screenDimensions;
+ const distanceX = event.translationX + event.velocityX * velocityFactor;
+ const distanceY = event.translationY + event.velocityY * velocityFactor;
+ const requiredXDistance = screenSize.width / 2;
+ const requiredYDistance = screenSize.height / 2;
+ const isTransitionCanceled = checkIfTransitionCancelled(
+ goBackGesture,
+ distanceX,
+ requiredXDistance,
+ distanceY,
+ requiredYDistance
+ );
+ const stackTag = screenTransitionConfig.value.stackTag;
+ screenTransitionConfig.value.onFinishAnimation = () => {
+ RNScreensTurboModule.finishTransition(stackTag, isTransitionCanceled);
+ };
+ screenTransitionConfig.value.isTransitionCanceled = isTransitionCanceled;
+ finishScreenTransition(screenTransitionConfig.value);
+ }
+
+ let panGesture = Gesture.Pan()
+ .onStart(onStart)
+ .onUpdate(onUpdate)
+ .onEnd(onEnd);
+
+ if (screenEdgeGesture) {
+ const HIT_SLOP_SIZE = 50;
+ const ACTIVATION_DISTANCE = 30;
+ if (goBackGesture === 'swipeRight') {
+ panGesture = panGesture
+ .activeOffsetX(ACTIVATION_DISTANCE)
+ .hitSlop({ left: 0, top: 0, width: HIT_SLOP_SIZE });
+ } else if (goBackGesture === 'swipeLeft') {
+ panGesture = panGesture
+ .activeOffsetX(-ACTIVATION_DISTANCE)
+ .hitSlop({ right: 0, top: 0, width: HIT_SLOP_SIZE });
+ } else if (goBackGesture === 'swipeDown') {
+ panGesture = panGesture
+ .activeOffsetY(ACTIVATION_DISTANCE)
+ .hitSlop({ top: 0, height: Dimensions.get('window').height * 0.2 });
+ // workaround, because we don't have access to header height
+ } else if (goBackGesture === 'swipeUp') {
+ panGesture = panGesture
+ .activeOffsetY(-ACTIVATION_DISTANCE)
+ .hitSlop({ bottom: 0, height: HIT_SLOP_SIZE });
+ }
+ }
+ return (
+
+ {children}
+
+ );
+};
+
+export default ScreenGestureDetector;
diff --git a/src/gesture-handler/constraints.ts b/src/gesture-handler/constraints.ts
new file mode 100644
index 0000000000..4d5577dfbd
--- /dev/null
+++ b/src/gesture-handler/constraints.ts
@@ -0,0 +1,87 @@
+import { ScreenTransition } from 'react-native-reanimated';
+import {
+ AnimatedScreenTransition,
+ GoBackGesture,
+ PanGestureHandlerEventPayload,
+} from '../native-stack/types';
+import { AnimationForGesture } from './defaults';
+import { GestureUpdateEvent } from 'react-native-gesture-handler';
+
+const SupportedGestures = [
+ 'swipeRight',
+ 'swipeLeft',
+ 'swipeDown',
+ 'swipeUp',
+ 'horizontalSwipe',
+ 'verticalSwipe',
+ 'twoDimensionalSwipe',
+];
+
+export function getAnimationForTransition(
+ goBackGesture: GoBackGesture | undefined,
+ customTransitionAnimation: AnimatedScreenTransition | undefined
+) {
+ let transitionAnimation = ScreenTransition.SwipeRight;
+ if (customTransitionAnimation) {
+ transitionAnimation = customTransitionAnimation;
+ if (!goBackGesture) {
+ throw new Error(
+ '[RNScreens] You have to specify `goBackGesture` when using `transitionAnimation`.'
+ );
+ }
+ } else {
+ if (!!goBackGesture && SupportedGestures.includes(goBackGesture)) {
+ transitionAnimation = AnimationForGesture[goBackGesture];
+ } else if (goBackGesture !== undefined) {
+ throw new Error(
+ `[RNScreens] Unknown goBackGesture parameter has been specified: ${goBackGesture}.`
+ );
+ }
+ }
+ return transitionAnimation;
+}
+
+export function checkBoundaries(
+ goBackGesture: string | undefined,
+ event: GestureUpdateEvent
+) {
+ 'worklet';
+ if (goBackGesture === 'swipeRight' && event.translationX < 0) {
+ event.translationX = 0;
+ } else if (goBackGesture === 'swipeLeft' && event.translationX > 0) {
+ event.translationX = 0;
+ } else if (goBackGesture === 'swipeDown' && event.translationY < 0) {
+ event.translationY = 0;
+ } else if (goBackGesture === 'swipeUp' && event.translationY > 0) {
+ event.translationY = 0;
+ }
+}
+
+export function checkIfTransitionCancelled(
+ goBackGesture: string | undefined,
+ distanceX: number,
+ requiredXDistance: number,
+ distanceY: number,
+ requiredYDistance: number
+) {
+ 'worklet';
+ let isTransitionCanceled = false;
+ if (goBackGesture === 'swipeRight') {
+ isTransitionCanceled = distanceX < requiredXDistance;
+ } else if (goBackGesture === 'swipeLeft') {
+ isTransitionCanceled = -distanceX < requiredXDistance;
+ } else if (goBackGesture === 'horizontalSwipe') {
+ isTransitionCanceled = Math.abs(distanceX) < requiredXDistance;
+ } else if (goBackGesture === 'swipeUp') {
+ isTransitionCanceled = -distanceY < requiredYDistance;
+ } else if (goBackGesture === 'swipeDown') {
+ isTransitionCanceled = distanceY < requiredYDistance;
+ } else if (goBackGesture === 'verticalSwipe') {
+ isTransitionCanceled = Math.abs(distanceY) < requiredYDistance;
+ } else if (goBackGesture === 'twoDimensionalSwipe') {
+ const isCanceledHorizontally = Math.abs(distanceX) < requiredXDistance;
+ const isCanceledVertically = Math.abs(distanceY) < requiredYDistance;
+ isTransitionCanceled = isCanceledHorizontally && isCanceledVertically;
+ }
+ return isTransitionCanceled;
+}
diff --git a/src/gesture-handler/defaults.ts b/src/gesture-handler/defaults.ts
new file mode 100644
index 0000000000..91f6d943b6
--- /dev/null
+++ b/src/gesture-handler/defaults.ts
@@ -0,0 +1,38 @@
+import {
+ GestureUpdateEvent,
+ PanGestureHandlerEventPayload,
+} from 'react-native-gesture-handler';
+import { ScreenTransition } from 'react-native-reanimated';
+
+export const DefaultEvent: GestureUpdateEvent = {
+ absoluteX: 0,
+ absoluteY: 0,
+ handlerTag: 0,
+ numberOfPointers: 0,
+ state: 0,
+ translationX: 0,
+ translationY: 0,
+ velocityX: 0,
+ velocityY: 0,
+ x: 0,
+ y: 0,
+};
+
+export const DefaultScreenDimensions = {
+ width: 0,
+ height: 0,
+ x: 0,
+ y: 0,
+ pageX: 0,
+ pageY: 0,
+};
+
+export const AnimationForGesture = {
+ swipeRight: ScreenTransition.SwipeRight,
+ swipeLeft: ScreenTransition.SwipeLeft,
+ swipeDown: ScreenTransition.SwipeDown,
+ swipeUp: ScreenTransition.SwipeUp,
+ horizontalSwipe: ScreenTransition.Horizontal,
+ verticalSwipe: ScreenTransition.Vertical,
+ twoDimensionalSwipe: ScreenTransition.TwoDimensional,
+};
diff --git a/src/gesture-handler/fabricUtils.ts b/src/gesture-handler/fabricUtils.ts
new file mode 100644
index 0000000000..976f04c761
--- /dev/null
+++ b/src/gesture-handler/fabricUtils.ts
@@ -0,0 +1,39 @@
+import { NativeStackNavigatorProps } from '../native-stack/types';
+
+interface HostInstance {
+ _internalInstanceHandle: {
+ stateNode: {
+ node: Record;
+ };
+ };
+ _nativeTag: number;
+}
+
+type LocalGlobal = typeof global & Record;
+
+export function isFabric() {
+ return !!(global as LocalGlobal)._IS_FABRIC;
+}
+
+let findHostInstance: (ref: React.Component) => HostInstance | null = () => {
+ return null;
+};
+if (isFabric()) {
+ try {
+ findHostInstance =
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ require('react-native/Libraries/Renderer/shims/ReactFabric').findHostInstance_DEPRECATED;
+ } catch (e) {
+ throw new Error('[RNScreens] Cannot import `findHostInstance_DEPRECATED`.');
+ }
+}
+
+export function getShadowNodeWrapperAndTagFromRef(
+ ref: React.Ref | React.Component
+) {
+ const hostInstance = findHostInstance(ref as React.Component);
+ return {
+ shadowNodeWrapper: hostInstance?._internalInstanceHandle.stateNode.node,
+ tag: hostInstance?._nativeTag,
+ };
+}
diff --git a/src/gesture-handler/fabricUtils.web.ts b/src/gesture-handler/fabricUtils.web.ts
new file mode 100644
index 0000000000..3d2fd9844c
--- /dev/null
+++ b/src/gesture-handler/fabricUtils.web.ts
@@ -0,0 +1,10 @@
+export function isFabric() {
+ return false;
+}
+
+export function getShadowNodeWrapperAndTagFromRef() {
+ return {
+ shadowNodeWrapper: undefined,
+ tag: undefined,
+ };
+}
diff --git a/src/gesture-handler/index.tsx b/src/gesture-handler/index.tsx
new file mode 100644
index 0000000000..22f8598e9a
--- /dev/null
+++ b/src/gesture-handler/index.tsx
@@ -0,0 +1 @@
+export { default as GestureDetectorProvider } from './GestureDetectorProvider';
diff --git a/src/index.native.tsx b/src/index.native.tsx
index 0e648e2771..16c308caf3 100644
--- a/src/index.native.tsx
+++ b/src/index.native.tsx
@@ -1,5 +1,11 @@
/* eslint-disable @typescript-eslint/no-var-requires */
-import React, { useEffect, PropsWithChildren, ReactNode } from 'react';
+import React, {
+ Fragment,
+ PropsWithChildren,
+ ReactNode,
+ useEffect,
+ useRef,
+} from 'react';
import {
Animated,
Image,
@@ -14,6 +20,7 @@ import {
} from 'react-native';
import { Freeze } from 'react-freeze';
import { version } from 'react-native/package.json';
+import NativeScreensModule from './fabric/NativeScreensModule';
import TransitionProgressContext from './TransitionProgressContext';
import useTransitionProgress from './useTransitionProgress';
@@ -210,7 +217,8 @@ function DelayedFreeze({ freeze, children }: FreezeWrapperProps) {
}
function ScreenStack(props: ScreenStackProps) {
- const { children, ...rest } = props;
+ const { children, gestureDetectorBridge, ...rest } = props;
+ const ref = useRef(null);
const size = React.Children.count(children);
// freezes all screens except the top one
const childrenWithFreeze = React.Children.map(children, (child, index) => {
@@ -226,8 +234,13 @@ function ScreenStack(props: ScreenStackProps) {
);
});
+ useEffect(() => {
+ if (gestureDetectorBridge) {
+ gestureDetectorBridge.current.stackUseEffectCallback(ref);
+ }
+ });
return (
-
+
{childrenWithFreeze}
);
@@ -567,6 +580,9 @@ export type {
// e.g. to use `useReanimatedTransitionProgress` (see `reanimated` folder in repo)
const ScreenContext = React.createContext(InnerScreen);
+// context to be used when the user wants full screen swipe (see `gesture-handler` folder in repo)
+const GHContext = React.createContext(Fragment);
+
class Screen extends React.Component {
static contextType = ScreenContext;
@@ -582,10 +598,14 @@ module.exports = {
Screen,
ScreenContainer,
ScreenContext,
+ GHContext,
ScreenStack,
InnerScreen,
SearchBar,
FullWindowOverlay,
+ get NativeScreensModule() {
+ return NativeScreensModule;
+ },
get NativeScreen() {
return ScreensNativeModules.NativeScreen;
diff --git a/src/index.tsx b/src/index.tsx
index 9e4e264fd9..76623abed7 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -8,6 +8,8 @@ import {
HeaderSubviewTypes,
SearchBarProps,
} from './types';
+import NativeScreensModule from './fabric/NativeScreensModule';
+import { GestureProviderProps } from './native-stack/types';
export * from './types';
export { default as useTransitionProgress } from './useTransitionProgress';
@@ -67,6 +69,10 @@ export const InnerScreen = View;
export const ScreenContext = React.createContext(Screen);
+export const GHContext = React.createContext(
+ (_props: GestureProviderProps): React.ReactElement => <>>
+);
+
export const ScreenContainer: React.ComponentType = View;
export const NativeScreenContainer: React.ComponentType =
@@ -117,3 +123,5 @@ export const ScreenStackHeaderSubview: React.ComponentType<
> = View;
export const shouldUseActivityState = true;
+
+export { NativeScreensModule };
diff --git a/src/native-stack/types.tsx b/src/native-stack/types.tsx
index 261af8b02c..722c6b1903 100644
--- a/src/native-stack/types.tsx
+++ b/src/native-stack/types.tsx
@@ -10,6 +10,7 @@ import {
RouteProp,
} from '@react-navigation/native';
import * as React from 'react';
+import { PropsWithChildren } from 'react';
import {
ImageSourcePropType,
StyleProp,
@@ -17,6 +18,7 @@ import {
ColorValue,
} from 'react-native';
import {
+ GestureDetectorBridge,
ScreenProps,
ScreenStackHeaderConfigProps,
SearchBarProps,
@@ -451,6 +453,10 @@ export type NativeStackNavigationOptions = {
* @platform ios
*/
transitionDuration?: number;
+
+ goBackGesture?: GoBackGesture;
+ transitionAnimation?: AnimatedScreenTransition;
+ screenEdgeGesture?: boolean;
};
export type NativeStackNavigatorProps =
@@ -468,3 +474,58 @@ export type NativeStackDescriptor = Descriptor<
export type NativeStackDescriptorMap = {
[key: string]: NativeStackDescriptor;
};
+
+// 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 = {
+ topScreenFrame: (
+ event: PanGestureHandlerEventPayload,
+ screenSize: MeasuredDimensions
+ ) => Record;
+ belowTopScreenFrame: (
+ event: PanGestureHandlerEventPayload,
+ screenSize: MeasuredDimensions
+ ) => Record;
+};
+
+export type ScreensRefsHolder = React.MutableRefObject<
+ Record>>
+>;
+
+export type GestureProviderProps = PropsWithChildren<{
+ gestureDetectorBridge: React.MutableRefObject;
+ screensRefs: ScreensRefsHolder;
+ currentRouteKey: string;
+ goBackGesture: GoBackGesture | undefined;
+ transitionAnimation: AnimatedScreenTransition | undefined;
+ screenEdgeGesture: boolean | undefined;
+}>;
diff --git a/src/native-stack/views/NativeStackView.tsx b/src/native-stack/views/NativeStackView.tsx
index 6e75c79d1d..3a4dc4fced 100644
--- a/src/native-stack/views/NativeStackView.tsx
+++ b/src/native-stack/views/NativeStackView.tsx
@@ -8,6 +8,8 @@ import {
ScreenStack,
StackPresentationTypes,
ScreenContext,
+ GHContext,
+ GestureDetectorBridge,
} from 'react-native-screens';
import {
ParamListBase,
@@ -26,6 +28,8 @@ import {
NativeStackDescriptorMap,
NativeStackNavigationHelpers,
NativeStackNavigationOptions,
+ NativeStackNavigatorProps,
+ ScreensRefsHolder,
} from '../types';
import HeaderConfig from './HeaderConfig';
import SafeAreaProviderCompat from '../utils/SafeAreaProviderCompat';
@@ -159,12 +163,14 @@ const RouteView = ({
index,
navigation,
stateKey,
+ screensRefs,
}: {
descriptors: NativeStackDescriptorMap;
route: NavigationRoute;
index: number;
navigation: NativeStackNavigationHelpers;
stateKey: string;
+ screensRefs: ScreensRefsHolder;
}) => {
const { options, render: renderScene } = descriptors[route.key];
const {
@@ -260,12 +266,21 @@ const RouteView = ({
).current;
const Screen = React.useContext(ScreenContext);
-
const { dark } = useTheme();
+ const screenRef = React.useRef(null);
+ React.useEffect(() => {
+ screensRefs.current[route.key] = screenRef;
+ return () => {
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
+ delete screensRefs.current[route.key];
+ };
+ });
+
return (
({
+ stackUseEffectCallback: _stackRef => {
+ // this method will be override in GestureDetector
+ },
+ });
+ type RefHolder = Record<
+ string,
+ React.MutableRefObject>
+ >;
+ const screensRefs = React.useRef({});
+ const ScreenGestureDetector = React.useContext(GHContext);
+
return (
-
- {routes.map((route, index) => (
-
- ))}
-
+
+
+ {routes.map((route, index) => (
+
+ ))}
+
+
);
}
diff --git a/src/types.tsx b/src/types.tsx
index 438b0d1d7f..aa29ad47c9 100644
--- a/src/types.tsx
+++ b/src/types.tsx
@@ -7,6 +7,7 @@ import {
TextInputFocusEventData,
ColorValue,
} from 'react-native';
+import { NativeStackNavigatorProps } from './native-stack/types';
export type SearchBarCommands = {
focus: () => void;
@@ -392,12 +393,20 @@ export interface ScreenContainerProps extends ViewProps {
hasTwoStates?: boolean;
}
+export interface GestureDetectorBridge {
+ stackUseEffectCallback: (
+ stackRef: React.MutableRefObject>
+ ) => void;
+}
+
export interface ScreenStackProps extends ViewProps {
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>;
}
export interface ScreenStackHeaderConfigProps extends ViewProps {
diff --git a/tsconfig.json b/tsconfig.json
index 1661811882..127a5742b1 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -4,7 +4,8 @@
"paths": {
"react-native-screens": ["./src"],
"react-native-screens/native-stack": ["./src/native-stack/index.tsx"],
- "react-native-screens/reanimated": ["src/reanimated/index.tsx"]
+ "react-native-screens/reanimated": ["src/reanimated/index.tsx"],
+ "react-native-screens/gesture-handler": ["src/gesture-handler/index.tsx"]
},
"allowUnreachableCode": false,
"allowUnusedLabels": false,
diff --git a/yarn.lock b/yarn.lock
index 1874c319ea..e7902aa1b1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1227,6 +1227,13 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
+"@egjs/hammerjs@^2.0.17":
+ version "2.0.17"
+ resolved "https://registry.yarnpkg.com/@egjs/hammerjs/-/hammerjs-2.0.17.tgz#5dc02af75a6a06e4c2db0202cae38c9263895124"
+ integrity sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==
+ dependencies:
+ "@types/hammerjs" "^2.0.36"
+
"@eslint-community/eslint-utils@^4.1.2", "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0":
version "4.4.0"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
@@ -2172,6 +2179,11 @@
dependencies:
"@types/node" "*"
+"@types/hammerjs@^2.0.36":
+ version "2.0.43"
+ resolved "https://registry.yarnpkg.com/@types/hammerjs/-/hammerjs-2.0.43.tgz#8660dd1e0e5fd979395e2f999e670cdb9484d1e9"
+ integrity sha512-wqxfwHk83RS7+6OpytGdo5wqkqtvx+bGaIs1Rwm5NrtQHUfL4OgWs/5p0OipmjmT+fexePh37Ek+mqIpdNjQKA==
+
"@types/http-cache-semantics@^4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz#0ea7b61496902b95890dc4c3a116b60cb8dae812"
@@ -5197,6 +5209,13 @@ hermes-profile-transformer@^0.0.6:
dependencies:
source-map "^0.7.3"
+hoist-non-react-statics@^3.3.0:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
+ integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
+ dependencies:
+ react-is "^16.7.0"
+
html-escaper@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
@@ -6712,11 +6731,6 @@ lodash.escaperegexp@^4.1.2:
resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347"
integrity sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==
-lodash.isequal@^4.5.0:
- version "4.5.0"
- resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
- integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==
-
lodash.isplainobject@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
@@ -8266,7 +8280,7 @@ react-freeze@^1.0.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
-react-is@^16.13.0, react-is@^16.13.1:
+react-is@^16.13.0, react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -8285,22 +8299,31 @@ react-native-codegen@^0.0.6:
jscodeshift "^0.11.0"
nullthrows "^1.1.1"
+react-native-gesture-handler@^2.13.3:
+ version "2.13.3"
+ resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.13.3.tgz#417dac3d063fd61589f21187ad732372d01c8feb"
+ integrity sha512-6sNXtmRfYxQWgH0TjQX03QQ4UfCyM8jX1Ee1jXc3uNgefk03qapGGxZ2noXodGWKHzpsqMxB0O1lFLGew0GRrw==
+ dependencies:
+ "@egjs/hammerjs" "^2.0.17"
+ hoist-non-react-statics "^3.3.0"
+ invariant "^2.2.4"
+ lodash "^4.17.21"
+ prop-types "^15.7.2"
+
react-native-iphone-x-helper@^1.3.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz#20c603e9a0e765fd6f97396638bdeb0e5a60b010"
integrity sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg==
-react-native-reanimated@^2.2.0:
- version "2.17.0"
- resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-2.17.0.tgz#eae2308235961cdd79810e01dfdd7e88b1ae5b5c"
- integrity sha512-bVy+FUEaHXq4i+aPPqzGeor1rG4scgVNBbBz21ohvC7iMpB9IIgvGsmy1FAoodZhZ5sa3EPF67Rcec76F1PXlQ==
+react-native-reanimated@3.7.0-nightly-20240109-9e2c33716:
+ version "3.7.0-nightly-20240109-9e2c33716"
+ resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-3.7.0-nightly-20240109-9e2c33716.tgz#ec647feabf1a0ba6673830f6ee2f3d57e0aa63b7"
+ integrity sha512-HMhmzQmAglaHuWwTiMVKx3fKZURi8oVP4u/76KYNBvUDK1Vh+sR54aFQ6d3X877LObw+R9vgr8LmajisY4poVQ==
dependencies:
"@babel/plugin-transform-object-assign" "^7.16.7"
"@babel/preset-typescript" "^7.16.7"
+ convert-source-map "^2.0.0"
invariant "^2.2.4"
- lodash.isequal "^4.5.0"
- setimmediate "^1.0.5"
- string-hash-64 "^1.0.3"
react-native-safe-area-context@^4.8.1:
version "4.8.1"
@@ -8929,11 +8952,6 @@ set-value@^2.0.0, set-value@^2.0.1:
is-plain-object "^2.0.3"
split-string "^3.0.1"
-setimmediate@^1.0.5:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
- integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==
-
setprototypeof@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
@@ -9229,11 +9247,6 @@ string-argv@0.3.2:
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6"
integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==
-string-hash-64@^1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/string-hash-64/-/string-hash-64-1.0.3.tgz#0deb56df58678640db5c479ccbbb597aaa0de322"
- integrity sha512-D5OKWKvDhyVWWn2x5Y9b+37NUllks34q1dCDhk/vYcso9fmhs+Tl3KR/gE4v5UNj2UA35cnX4KdVVGkG1deKqw==
-
string-length@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a"