├── .babelrc
├── .circleci
└── config.yml
├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .release-it.json
├── LICENSE.md
├── README.md
├── example
├── .babelrc
├── .buckconfig
├── .eslintrc
├── .watchmanconfig
├── App.js
├── README.md
├── app.json
├── package.json
├── rn-cli.config.js
├── src
│ ├── CardTransition.js
│ ├── FadeTransition.js
│ ├── LayoutContext.js
│ └── examples
│ │ ├── CardStack.js
│ │ ├── Fade.js
│ │ ├── Gesture.js
│ │ ├── Modal.js
│ │ └── SharedEl.js
└── yarn.lock
├── jest-setup.js
├── package.json
├── src
├── Shared.js
├── Transitioner.js
├── createStackTransitionNavigator.js
└── index.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["react-native"]
3 | }
4 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | defaults: &defaults
4 | docker:
5 | - image: circleci/node:7.10
6 | working_directory: ~/project
7 |
8 | jobs:
9 | install-dependencies:
10 | <<: *defaults
11 | steps:
12 | - checkout
13 | - attach_workspace:
14 | at: ~/project
15 | - restore_cache:
16 | keys:
17 | - v1-dependencies-{{ checksum "package.json" }}
18 | - v1-dependencies-
19 | - restore_cache:
20 | keys:
21 | - v1-dependencies-example-{{ checksum "example/package.json" }}
22 | - v1-dependencies-example-
23 | - run: |
24 | yarn install
25 | yarn install --cwd example
26 | - save_cache:
27 | key: v1-dependencies-{{ checksum "package.json" }}
28 | paths: node_modules
29 | - save_cache:
30 | key: v1-dependencies-example-{{ checksum "example/package.json" }}
31 | paths: example/node_modules
32 | - persist_to_workspace:
33 | root: .
34 | paths: .
35 | lint:
36 | <<: *defaults
37 | steps:
38 | - attach_workspace:
39 | at: ~/project
40 | - run: |
41 | yarn run lint
42 | unit-tests:
43 | <<: *defaults
44 | steps:
45 | - attach_workspace:
46 | at: ~/project
47 | - run: yarn test -- --coverage
48 | - store_artifacts:
49 | path: coverage
50 | destination: coverage
51 |
52 | workflows:
53 | version: 2
54 | build-and-test:
55 | jobs:
56 | - install-dependencies
57 | - lint:
58 | requires:
59 | - install-dependencies
60 | - unit-tests:
61 | requires:
62 | - install-dependencies
63 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 |
8 | [*]
9 |
10 | # change these settings to your own preference
11 | indent_style = space
12 | indent_size = 2
13 |
14 | # we recommend you to keep these unchanged
15 | end_of_line = lf
16 | charset = utf-8
17 | trim_trailing_whitespace = true
18 | insert_final_newline = true
19 |
20 | [*.md]
21 | trim_trailing_whitespace = false
22 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 | jest-setup.js
4 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint-config-satya164",
3 |
4 | "plugins": ["react-native-globals"],
5 |
6 | "env": {
7 | "es6": true,
8 | "react-native-globals/all": true,
9 | },
10 |
11 | "rules": {
12 | "import/no-unresolved": "off",
13 | "react/sort-comp": "off",
14 | "jest/no-disabled-tests": "off",
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | #
3 | .DS_Store
4 |
5 | # XDE
6 | .expo/
7 |
8 | # VSCode
9 | .vscode/
10 | tsconfig.json
11 | jsconfig.json
12 |
13 | # Xcode
14 | #
15 | build/
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 | xcuserdata
25 | *.xccheckout
26 | *.moved-aside
27 | DerivedData
28 | *.hmap
29 | *.ipa
30 | *.xcuserstate
31 | project.xcworkspace
32 |
33 | # Android/IJ
34 | #
35 | .idea
36 | .gradle
37 | local.properties
38 |
39 | # node.js
40 | #
41 | node_modules/
42 | npm-debug.log
43 | yarn-debug.log
44 | yarn-error.log
45 |
46 | # BUCK
47 | buck-out/
48 | \.buckd/
49 | android/app/libs
50 | android/keystores/debug.keystore
51 |
52 | # Build
53 | dist/
54 |
--------------------------------------------------------------------------------
/.release-it.json:
--------------------------------------------------------------------------------
1 | {
2 | "increment": "conventional:angular",
3 | "changelogCommand": "conventional-changelog -p angular | tail -n +3",
4 | "safeBump": false,
5 | "src": {
6 | "commitMessage": "chore: release %s",
7 | "tagName": "v%s"
8 | },
9 | "npm": {
10 | "publish": true
11 | },
12 | "github": {
13 | "release": true
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 React Native Community
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Navigation Transitioner
2 |
3 | [](https://circleci.com/gh/react-navigation/react-navigation-transitioner/tree/master)
4 |
5 | Stack navigator for use on iOS and Android.
6 |
7 | ## Installation
8 |
9 | Open a Terminal in your project's folder and run,
10 |
11 | ```sh
12 | yarn add react-navigation-transitioner
13 | ```
14 |
15 | ## Usage
16 |
17 | Coming soon, see examples.
18 |
--------------------------------------------------------------------------------
/example/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["expo"],
3 | "plugins": [
4 | [
5 | "module-resolver",
6 | {
7 | "alias": {
8 | "react-navigation-transitioner": "../src"
9 | }
10 | }
11 | ]
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/example/.buckconfig:
--------------------------------------------------------------------------------
1 |
2 | [android]
3 | target = Google Inc.:Google APIs:23
4 |
5 | [maven_repositories]
6 | central = https://repo1.maven.org/maven2
7 |
--------------------------------------------------------------------------------
/example/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../.eslintrc",
3 |
4 | "settings": {
5 | "import/core-modules": ["expo", "react-navigation-transitioner"]
6 | },
7 |
8 | "rules": {
9 | "react/prop-types": "off"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/example/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/example/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Expo from 'expo';
3 |
4 | import { View, Button } from 'react-native';
5 | import { createSwitchNavigator } from '@react-navigation/core';
6 | import { createAppContainer } from '@react-navigation/native';
7 | import Fade from './src/examples/Fade';
8 | import Modal from './src/examples/Modal';
9 | import Gesture from './src/examples/Gesture';
10 | import CardStack from './src/examples/CardStack';
11 | import SharedEl from './src/examples/SharedEl';
12 |
13 | import { Provider as LayoutProvider } from './src/LayoutContext';
14 |
15 | process.env.REACT_NAV_LOGGING = true;
16 |
17 | const Examples = ({ navigation }) => (
18 |
19 | {Object.keys(EXAMPLES).map(exampleName => (
20 |
27 | );
28 |
29 | const EXAMPLES = {
30 | Fade,
31 | Modal,
32 | Gesture,
33 | CardStack,
34 | SharedEl,
35 | };
36 |
37 | const AppNavigator = createSwitchNavigator({
38 | Examples,
39 | ...EXAMPLES,
40 | });
41 |
42 | const StatefulAppNavigator = createAppContainer(AppNavigator);
43 |
44 | // const StatefulAppNavigator = createAppContainer(Fade);
45 | // const StatefulAppNavigator = createAppContainer(Modal);
46 | // const StatefulAppNavigator = createAppContainer(Gesture);
47 | // const StatefulAppNavigator = createAppContainer(CardStack);
48 | // const StatefulAppNavigator = createAppContainer(SharedEl);
49 |
50 | const App = () => (
51 |
52 |
53 |
54 | );
55 |
56 | Expo.registerRootComponent(App);
57 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | ## Run the example
2 |
3 | - Run the example locally
4 | - Clone the repository and `cd` to this directory
5 | - Run `yarn` to install the dependencies
6 | - Run `yarn start` to start the packager
7 | - Scan the QR Code with the Expo app
8 |
--------------------------------------------------------------------------------
/example/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "React Navigation Transitioner Example",
4 | "description": "Demonstrates the various capabilities of react-navigation-transitioner",
5 | "slug": "react-navigation-transitioner-demo",
6 | "sdkVersion": "30.0.0",
7 | "version": "1.0.0",
8 | "primaryColor": "#2196f3",
9 | "packagerOpts": {
10 | "assetExts": ["ttf"],
11 | "config": "./rn-cli.config.js",
12 | "projectRoots": ""
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "transitionerexamples",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "start": "expo start",
7 | "android": "expo start --android",
8 | "ios": "expo start --ios",
9 | "postinstall": "rm -rf node_modules/expo-react-native-adapter/node_modules/react && rm -rf node_modules/expo-gl/node_modules/react"
10 | },
11 | "dependencies": {
12 | "expo": "~30.0.0",
13 | "react-native": "~0.55.4",
14 | "@react-navigation/core": "^3.0.0-alpha.7",
15 | "@react-navigation/native": "^3.0.0-alpha.5",
16 | "react": "16.3.1"
17 | },
18 | "devDependencies": {
19 | "babel-plugin-module-resolver": "^3.0.0",
20 | "babel-preset-expo": "^4.0.0",
21 | "glob-to-regexp": "^0.3.0"
22 | },
23 | "main": "App.js",
24 | "resolutions": {
25 | "**/react": "16.3.1",
26 | "**/prop-types": "15.6.0",
27 | "**/react-lifecycles-compat": "3.0.4",
28 | "**/hoist-non-react-statics": "2.5.0"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/example/rn-cli.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-commonjs */
2 |
3 | const path = require('path');
4 | const glob = require('glob-to-regexp');
5 | const blacklist = require('metro/src/blacklist');
6 | const pak = require('../package.json');
7 | const pak2 = require('./package.json');
8 |
9 | const dependencies = Object.keys(pak.dependencies);
10 | const localDependencies = Object.keys(pak2.dependencies);
11 | const peerDependencies = Object.keys(pak.peerDependencies);
12 |
13 | module.exports = {
14 | getProjectRoots() {
15 | return [__dirname, path.resolve(__dirname, '..')];
16 | },
17 | getProvidesModuleNodeModules() {
18 | return [...dependencies, ...localDependencies, ...peerDependencies];
19 | },
20 | getBlacklistRE() {
21 | return blacklist([glob(`${path.resolve(__dirname, '..')}/node_modules/*`)]);
22 | },
23 | };
24 |
--------------------------------------------------------------------------------
/example/src/CardTransition.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Dimensions } from 'react-native';
3 | import Animated from 'react-native-reanimated';
4 | import { PanGestureHandler, State } from 'react-native-gesture-handler';
5 | import { Consumer } from './LayoutContext';
6 |
7 | const {
8 | multiply,
9 | add,
10 | sub,
11 | eq,
12 | neq,
13 | cond,
14 | spring,
15 | not,
16 | block,
17 | call,
18 | event,
19 | Value,
20 | Clock,
21 | clockRunning,
22 | stopClock,
23 | startClock,
24 | set,
25 | lessThan,
26 | divide,
27 | greaterThan,
28 | and,
29 | interpolate,
30 | } = Animated;
31 | const callWhenTrue = (val, callback) => cond(val, call([val], callback));
32 |
33 | const TOSS_VELOCITY_MULTIPLIER = 0.5;
34 |
35 | export default class CardTransition extends React.Component {
36 | static navigationOptions = {
37 | getBehindTransitionAnimatedStyle: transition => {
38 | return {
39 | opacity: interpolate(transition.progress, {
40 | inputRange: [0, 1],
41 | outputRange: [1, 0.85],
42 | }),
43 | transform: [
44 | {
45 | translateX: multiply(
46 | -0.5,
47 | multiply(transition.screenWidth, transition.progress)
48 | ),
49 | },
50 | ],
51 | };
52 | },
53 | createTransition: transition => {
54 | const clock = new Clock();
55 | const gestureState = new Value(State.END);
56 | const gestureTranslateX = new Value(0);
57 | const prevGestureTranslateX = new Value(0);
58 | const gestureVelocityX = new Value(0);
59 | const targetProgress = new Value(0);
60 | const screenWidth = new Value(Dimensions.get('window').width);
61 | const lastGestureTranslateX = new Value(0);
62 | const lastGestureVelocityX = new Value(0);
63 |
64 | const isClosing = and(
65 | neq(gestureState, State.ACTIVE),
66 | lessThan(
67 | add(
68 | lastGestureTranslateX,
69 | multiply(TOSS_VELOCITY_MULTIPLIER, lastGestureVelocityX)
70 | ),
71 | -100
72 | )
73 | );
74 | const targetProgressDistance = multiply(
75 | cond(isClosing, 0, targetProgress),
76 | screenWidth
77 | );
78 | const progressDistance = new Value(0);
79 | const isAtRest = and(
80 | not(clockRunning(clock)),
81 | neq(gestureState, State.ACTIVE)
82 | );
83 | const uprightGestureTranslateX = multiply(gestureTranslateX, -1);
84 | const uprightGestureVelocityX = multiply(gestureVelocityX, -1);
85 | const state = {
86 | finished: new Value(0),
87 | velocity: new Value(0),
88 | position: progressDistance,
89 | time: new Value(0),
90 | };
91 |
92 | const config = {
93 | stiffness: 1000,
94 | damping: 600,
95 | mass: 3,
96 | overshootClamping: true,
97 | restSpeedThreshold: 0.1,
98 | restDisplacementThreshold: 1,
99 | toValue: targetProgressDistance,
100 | };
101 |
102 | const goSpring = [
103 | cond(clockRunning(clock), 0, [
104 | set(state.finished, 0),
105 | set(state.velocity, uprightGestureVelocityX),
106 | startClock(clock),
107 | ]),
108 | spring(clock, state, config),
109 | cond(state.finished, stopClock(clock)),
110 | ];
111 | const gestureProgressDistance = add(
112 | progressDistance,
113 | sub(uprightGestureTranslateX, prevGestureTranslateX)
114 | );
115 | const clampedGestureProgressDistance = cond(
116 | greaterThan(screenWidth, gestureProgressDistance),
117 | gestureProgressDistance,
118 | screenWidth
119 | );
120 | const springProgressDistance = block([
121 | cond(
122 | eq(gestureState, State.ACTIVE),
123 | [
124 | stopClock(clock),
125 | set(progressDistance, clampedGestureProgressDistance),
126 | set(prevGestureTranslateX, uprightGestureTranslateX),
127 | set(lastGestureTranslateX, uprightGestureTranslateX),
128 | set(lastGestureVelocityX, uprightGestureVelocityX),
129 | ],
130 | [set(prevGestureTranslateX, 0), goSpring]
131 | ),
132 | progressDistance,
133 | ]);
134 | let callbacksWaitingForRest = [];
135 | const whenDoneCallback = () => {
136 | callbacksWaitingForRest.forEach(cb => cb());
137 | callbacksWaitingForRest = [];
138 | };
139 | const waitForDone = () =>
140 | new Promise(resolve => {
141 | callbacksWaitingForRest.push(resolve);
142 | });
143 | const closingCallback = () => {
144 | transition.navigation.goBack(transition.transitionRouteKey);
145 | };
146 | const finalDistanceProgress = block([
147 | springProgressDistance,
148 | callWhenTrue(isAtRest, whenDoneCallback),
149 | callWhenTrue(isClosing, closingCallback),
150 | springProgressDistance,
151 | ]);
152 | return {
153 | ...transition,
154 | screenWidth,
155 | gestureState,
156 | gestureTranslateX,
157 | gestureVelocityX,
158 | targetProgress,
159 | progress: divide(finalDistanceProgress, screenWidth),
160 | waitForDone,
161 | };
162 | },
163 | runTransition: async (transition, _, fromState, toState) => {
164 | const destVal = toState.index >= fromState.index ? 1 : 0;
165 | transition.targetProgress.setValue(destVal);
166 |
167 | await transition.waitForDone();
168 | },
169 | };
170 | _renderCard = styles => {
171 | return (
172 |
183 | {this.props.children}
184 |
185 | );
186 | };
187 | render() {
188 | return {this._renderWithLayout};
189 | }
190 | _renderWithLayout = layout => {
191 | const { transition, navigation } = this.props;
192 | const myKey = navigation.state.key;
193 | if (!transition || transition.transitionRouteKey !== myKey) {
194 | return this._renderCard({});
195 | }
196 | const {
197 | screenWidth,
198 | progress,
199 | gestureState,
200 | gestureTranslateX,
201 | gestureVelocityX,
202 | } = transition;
203 | screenWidth.setValue(layout.width);
204 |
205 | const styles = {
206 | transform: [
207 | {
208 | translateX: interpolate(progress, {
209 | inputRange: [0, 1],
210 | outputRange: [layout.width, 0],
211 | }),
212 | },
213 | ],
214 | shadowOpacity: interpolate(progress, {
215 | inputRange: [0, 1],
216 | outputRange: [0, 0.2],
217 | extrapolate: 'clamp',
218 | }),
219 | };
220 |
221 | return (
222 |
240 | {this._renderCard(styles)}
241 |
242 | );
243 | };
244 | }
245 |
--------------------------------------------------------------------------------
/example/src/FadeTransition.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Animated, { Easing } from 'react-native-reanimated';
3 | const { Value, timing } = Animated;
4 |
5 | export default class FadeTransition extends React.Component {
6 | static navigationOptions = {
7 | createTransition: transition => ({
8 | ...transition,
9 | opacity: new Value(0),
10 | }),
11 | runTransition: async (transition, _, fromState, toState) => {
12 | const isVisible = !!toState.routes.find(
13 | r => r.key === transition.transitionRouteKey
14 | );
15 | await new Promise(resolve => {
16 | timing(transition.opacity, {
17 | toValue: isVisible ? 1 : 0,
18 | duration: 500,
19 | easing: Easing.inOut(Easing.cubic),
20 | }).start(resolve);
21 | });
22 | },
23 | };
24 | render() {
25 | const { transition } = this.props;
26 | let opacity = 1;
27 | if (transition) {
28 | opacity = transition.opacity;
29 | }
30 | return (
31 |
32 | {this.props.children}
33 |
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/example/src/LayoutContext.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, View, SafeAreaView, Keyboard } from 'react-native';
3 | import Animated from 'react-native-reanimated';
4 |
5 | const LayoutReactContext = React.createContext(null);
6 |
7 | const setAnimatedValue = (animatedValues, stateObj, valueName) => {
8 | const val = stateObj[valueName];
9 | if (typeof val === 'number') {
10 | animatedValues[valueName].setValue(val);
11 | }
12 | };
13 |
14 | class InternalLayoutProvider extends React.Component {
15 | state = {
16 | safeTop: 0,
17 | safeLeft: 0,
18 | safeBottom: 0,
19 | safeRight: 0,
20 | width: null,
21 | height: null,
22 | _kbScreenY: null,
23 | _hasMeasuredContainer: false,
24 | _hasMeasuredSafeArea: false,
25 | };
26 | animated = {
27 | width: new Animated.Value(0),
28 | height: new Animated.Value(0),
29 | safeTop: new Animated.Value(0),
30 | safeBottom: new Animated.Value(0),
31 | safeLeft: new Animated.Value(0),
32 | safeRight: new Animated.Value(0),
33 | };
34 | componentDidMount() {
35 | if (!this.props.parentLayout) {
36 | this._kbWSSub = Keyboard.addListener('keyboardWillShow', e => {
37 | console.log('keyboardWillShow', e.endCoordinates.screenY);
38 | this._updateKBScreenY(e.endCoordinates.screenY);
39 | });
40 | this._kbDSSub = Keyboard.addListener('keyboardDidShow', e => {
41 | console.log('keyboardDidShow', e.endCoordinates.screenY);
42 | this._updateKBScreenY(e.endCoordinates.screenY);
43 | });
44 | this._kbWHSub = Keyboard.addListener('keyboardWillHide', e => {
45 | console.log('keyboardWillHide', e.endCoordinates.screenY);
46 | this._updateKBScreenY(e.endCoordinates.screenY);
47 | });
48 | this._kbDHSub = Keyboard.addListener('keyboardDidHide', e => {
49 | console.log('keyboardDidHide', e.endCoordinates.screenY);
50 | this._updateKBScreenY(e.endCoordinates.screenY);
51 | });
52 | }
53 | }
54 | _updateKBScreenY = _kbScreenY => {
55 | this._setComputedState({
56 | _kbScreenY,
57 | });
58 | };
59 | componentWillUnmount() {
60 | this._kbWSSub && this._kbWSSub.remove();
61 | this._kbDSSub && this._kbDSSub.remove();
62 | this._kbWHSub && this._kbWHSub.remove();
63 | this._kbDHSub && this._kbDHSub.remove();
64 | }
65 | _setComputedState = newState => {
66 | const state = { ...this.state, ...newState };
67 |
68 | state.safeLeft = state._safeViewX;
69 | state.safeRight = state.width - state._safeViewX - state._safeViewWidth;
70 | state.safeTop = state._safeViewY;
71 | state.safeBottom = state.height - state._safeViewY - state._safeViewHeight;
72 | if (state._kbScreenY !== null) {
73 | const kbSafeBottom = state.height - state._kbScreenY;
74 | state.safeBottom = Math.max(state.safeBottom, kbSafeBottom);
75 | }
76 | this._updateAnimatedValues(state);
77 | if (state.width === null || state.height === null) {
78 | this.state = state; // eslint-disable-line react/no-direct-mutation-state
79 | } else {
80 | this.setState(state);
81 | }
82 | };
83 | _updateAnimatedValues = state => {
84 | setAnimatedValue(this.animated, state, 'width');
85 | setAnimatedValue(this.animated, state, 'height');
86 | setAnimatedValue(this.animated, state, 'safeLeft');
87 | setAnimatedValue(this.animated, state, 'safeRight');
88 | setAnimatedValue(this.animated, state, 'safeTop');
89 | setAnimatedValue(this.animated, state, 'safeBottom');
90 | };
91 |
92 | _onContainerLayout = e => {
93 | this._setComputedState({
94 | width: e.nativeEvent.layout.width,
95 | height: e.nativeEvent.layout.height,
96 | _containerX: e.nativeEvent.layout.x,
97 | _containerY: e.nativeEvent.layout.y,
98 | _hasMeasuredContainer: true,
99 | });
100 | };
101 | _onSafeLayout = e => {
102 | this._setComputedState({
103 | _safeViewHeight: e.nativeEvent.layout.height,
104 | _safeViewWidth: e.nativeEvent.layout.width,
105 | _safeViewX: e.nativeEvent.layout.x,
106 | _safeViewY: e.nativeEvent.layout.y,
107 | _hasMeasuredSafeArea: true,
108 | });
109 | };
110 | _renderContainerContent = () => {
111 | const { height, _safeViewHeight, _safeViewY, _containerY } = this.state;
112 | const contentContainerStyle = {
113 | flex: 1,
114 | };
115 | const { inset, parentLayout } = this.props;
116 | if (inset) {
117 | // contentContainerStyle.paddingTop = parentLayout.safeTop;
118 | contentContainerStyle.paddingTop = Math.min(
119 | parentLayout.safeTop,
120 | _safeViewY - _containerY
121 | );
122 | contentContainerStyle.paddingBottom = Math.min(
123 | parentLayout.safeBottom,
124 | _safeViewHeight - height - _containerY
125 | );
126 | contentContainerStyle.paddingLeft = parentLayout.safeLeft;
127 | contentContainerStyle.paddingRight = parentLayout.safeRight;
128 | }
129 | return (
130 |
133 | {this.props.children}
134 |
135 | );
136 | };
137 | _renderHackyPart = () => {
138 | return (
139 |
152 | );
153 | };
154 | _renderContainerMeasuring = () => {
155 | const color = this.props.debugColor;
156 | if (!this.state._hasMeasuredContainer) {
157 | return null;
158 | }
159 | return (
160 |
161 |
162 |
163 |
164 |
165 |
166 | {this._renderHackyPart()}
167 | {this._renderContainerContent()}
168 | {color && (
169 |
182 |
195 |
196 | )}
197 |
198 | );
199 | };
200 | render() {
201 | const { inset, parentLayout, style } = this.props;
202 | if (inset && !parentLayout) {
203 | throw 'Cannot inset top level layout context provider';
204 | }
205 | const containerStyle = { ...StyleSheet.flatten(style) };
206 | return (
207 |
208 | {this._renderContainerMeasuring()}
209 |
210 | );
211 | }
212 | }
213 |
214 | class LayoutProvider extends React.Component {
215 | render() {
216 | return (
217 |
218 | {parentLayout => (
219 |
220 | )}
221 |
222 | );
223 | }
224 | }
225 |
226 | export const KeyboardAvoiding = ({ children, debugColor, style }) => (
227 |
228 | {children}
229 |
230 | );
231 |
232 | export const InsetView = ({ children, debugColor, style }) => (
233 |
234 | {children}
235 |
236 | );
237 |
238 | export const Provider = LayoutProvider;
239 | export const Consumer = LayoutReactContext.Consumer;
240 |
--------------------------------------------------------------------------------
/example/src/examples/CardStack.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Button,
4 | Text as UnstyledText,
5 | View as UnstyledView,
6 | } from 'react-native';
7 | import { createNavigator, StackRouter } from '@react-navigation/core';
8 | import { Transitioner } from 'react-navigation-transitioner';
9 | import CardTransition from '../CardTransition';
10 |
11 | const View = props => (
12 |
16 | );
17 | const Text = props => (
18 |
19 | );
20 |
21 | class HomeScreen extends React.Component {
22 | static navigationOptions = CardTransition.navigationOptions;
23 | render() {
24 | const { navigation } = this.props;
25 | return (
26 |
27 |
28 | Home Screen
29 |
42 |
43 | );
44 | }
45 | }
46 |
47 | class ProfileScreen extends React.Component {
48 | static navigationOptions = CardTransition.navigationOptions;
49 | render() {
50 | const { navigation } = this.props;
51 | return (
52 |
53 |
54 |
55 | {navigation.getParam('name')}
56 | 's Profile
57 |
58 | navigation.push('HomeScreen')}
60 | title="Go Home"
61 | />
62 | navigation.goBack()} title="Go Back" />
63 |
64 |
65 | );
66 | }
67 | }
68 |
69 | const App = createNavigator(
70 | Transitioner,
71 | StackRouter({
72 | HomeScreen,
73 | ProfileScreen,
74 | }),
75 | {}
76 | );
77 |
78 | export default App;
79 |
--------------------------------------------------------------------------------
/example/src/examples/Fade.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Button,
4 | Text as UnstyledText,
5 | View as UnstyledView,
6 | } from 'react-native';
7 | import { createNavigator, StackRouter } from '@react-navigation/core';
8 | import { Transitioner } from 'react-navigation-transitioner';
9 | import FadeTransition from '../FadeTransition';
10 |
11 | const View = props => (
12 |
16 | );
17 | const Text = props => (
18 |
19 | );
20 |
21 | const HomeScreen = ({ navigation }) => (
22 |
23 | Home Screen
24 | {
26 | navigation.navigate('ProfileScreen', { name: 'Jane' });
27 | }}
28 | title="Go to Jane's profile"
29 | />
30 | {
32 | navigation.navigate('Examples');
33 | }}
34 | title="Exit"
35 | />
36 |
37 | );
38 |
39 | class ProfileScreen extends React.Component {
40 | static navigationOptions = FadeTransition.navigationOptions;
41 | render() {
42 | const { navigation } = this.props;
43 | return (
44 |
45 |
46 |
47 | {navigation.getParam('name')}
48 | 's Profile
49 |
50 | navigation.push('HomeScreen')}
52 | title="Go Home"
53 | />
54 | navigation.goBack()} title="Go Back" />
55 |
56 |
57 | );
58 | }
59 | }
60 |
61 | const App = createNavigator(
62 | Transitioner,
63 | StackRouter({
64 | HomeScreen,
65 | ProfileScreen,
66 | }),
67 | {}
68 | );
69 |
70 | export default App;
71 |
--------------------------------------------------------------------------------
/example/src/examples/Gesture.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Button,
4 | Text as UnstyledText,
5 | View as UnstyledView,
6 | StyleSheet,
7 | TouchableWithoutFeedback,
8 | } from 'react-native';
9 | import { createNavigator, StackRouter } from '@react-navigation/core';
10 | import { Transitioner } from 'react-navigation-transitioner';
11 | import Animated from 'react-native-reanimated';
12 | import { PanGestureHandler, State } from 'react-native-gesture-handler';
13 | const {
14 | Value,
15 | interpolate,
16 | Clock,
17 | add,
18 | multiply,
19 | divide,
20 | cond,
21 | lessThan,
22 | set,
23 | clockRunning,
24 | startClock,
25 | stopClock,
26 | eq,
27 | sub,
28 | not,
29 | spring,
30 | neq,
31 | block,
32 | call,
33 | event,
34 | and,
35 | } = Animated;
36 |
37 | const MODAL_TRANSLATE_DIST = 300;
38 | const TOSS_VELOCITY_MULTIPLIER = 0.2;
39 |
40 | const callWhenTrue = (val, callback) => cond(val, call([val], callback));
41 |
42 | class GestureTransition extends React.Component {
43 | static navigationOptions = {
44 | createTransition: transition => {
45 | const clock = new Clock();
46 | const gestureState = new Value(State.END);
47 | const gestureTranslateY = new Value(0);
48 | const prevGestureTranslateY = new Value(0);
49 | const gestureVelocityY = new Value(0);
50 | const targetProgress = new Value(0);
51 |
52 | const progressDistance = new Value(0);
53 | const isAtRest = and(
54 | not(clockRunning(clock)),
55 | neq(gestureState, State.ACTIVE)
56 | );
57 | const uprightGestureTranslateY = multiply(gestureTranslateY, -1);
58 | const uprightGestureVelocityY = multiply(gestureVelocityY, -1);
59 | const lastGestureTranslateY = new Value(0);
60 | const lastGestureVelocityY = new Value(0);
61 | const state = {
62 | finished: new Value(0),
63 | velocity: new Value(0),
64 | position: progressDistance,
65 | time: new Value(0),
66 | };
67 |
68 | const isClosing = and(
69 | neq(gestureState, State.ACTIVE),
70 | lessThan(
71 | add(
72 | lastGestureTranslateY,
73 | multiply(TOSS_VELOCITY_MULTIPLIER, lastGestureVelocityY)
74 | ),
75 | -100
76 | )
77 | );
78 | const targetProgressDistance = multiply(
79 | cond(isClosing, 0, targetProgress),
80 | MODAL_TRANSLATE_DIST
81 | );
82 | const config = {
83 | stiffness: 1000,
84 | damping: 500,
85 | mass: 3,
86 | overshootClamping: true,
87 | restSpeedThreshold: 0.01,
88 | restDisplacementThreshold: 1,
89 | toValue: targetProgressDistance,
90 | };
91 |
92 | const goSpring = [
93 | cond(clockRunning(clock), 0, [
94 | set(state.finished, 0),
95 | set(state.velocity, uprightGestureVelocityY),
96 | startClock(clock),
97 | ]),
98 | spring(clock, state, config),
99 | cond(state.finished, stopClock(clock)),
100 | ];
101 |
102 | const springProgressDistance = block([
103 | state.position,
104 | cond(
105 | eq(gestureState, State.ACTIVE),
106 | [
107 | stopClock(clock),
108 | set(
109 | progressDistance,
110 | add(
111 | progressDistance,
112 | sub(uprightGestureTranslateY, prevGestureTranslateY)
113 | )
114 | ),
115 | set(prevGestureTranslateY, uprightGestureTranslateY),
116 | set(lastGestureTranslateY, uprightGestureTranslateY),
117 | set(lastGestureVelocityY, uprightGestureVelocityY),
118 | ],
119 | [set(prevGestureTranslateY, 0), goSpring]
120 | ),
121 | progressDistance,
122 | ]);
123 | let callbacksWaitingForRest = [];
124 | const whenDoneCallback = () => {
125 | callbacksWaitingForRest.forEach(cb => cb());
126 | callbacksWaitingForRest = [];
127 | };
128 | const waitForRest = () =>
129 | new Promise(resolve => {
130 | callbacksWaitingForRest.push(resolve);
131 | });
132 | const closingCallback = () => {
133 | transition.navigation.goBack(transition.transitionRouteKey);
134 | };
135 |
136 | const finalDistanceProgress = block([
137 | springProgressDistance,
138 | callWhenTrue(isAtRest, whenDoneCallback),
139 | callWhenTrue(isClosing, closingCallback),
140 | springProgressDistance,
141 | ]);
142 | return {
143 | ...transition,
144 | gestureState,
145 | gestureTranslateY,
146 | gestureVelocityY,
147 | targetProgress,
148 | progress: divide(finalDistanceProgress, MODAL_TRANSLATE_DIST),
149 | waitForRest,
150 | };
151 | },
152 | runTransition: async (transition, _, fromState, toState) => {
153 | const destVal = toState.index >= fromState.index ? 1 : 0;
154 | transition.targetProgress.setValue(destVal);
155 |
156 | await transition.waitForRest();
157 | },
158 | };
159 | _renderModal = (transform, opacity) => {
160 | const { navigation } = this.props;
161 | return (
162 |
163 | navigation.goBack()}>
164 |
170 |
171 |
181 | {this.props.children}
182 |
183 |
184 | );
185 | };
186 | render() {
187 | const { transition } = this.props;
188 | if (!transition) {
189 | return this._renderModal([], 1);
190 | }
191 | const {
192 | progress,
193 | gestureState,
194 | gestureTranslateY,
195 | gestureVelocityY,
196 | } = transition;
197 | const opacity = interpolate(progress, {
198 | inputRange: [0, 1],
199 | outputRange: [0, 1],
200 | });
201 | const transform = [
202 | {
203 | translateY: interpolate(progress, {
204 | inputRange: [0, 1],
205 | outputRange: [300, 0],
206 | }),
207 | },
208 | ];
209 |
210 | return (
211 |
229 | {this._renderModal(transform, opacity)}
230 |
231 | );
232 | }
233 | }
234 |
235 | const View = props => (
236 |
240 | );
241 | const Text = props => (
242 |
243 | );
244 |
245 | class HomeScreen extends React.Component {
246 | render() {
247 | const { navigation } = this.props;
248 | return (
249 |
250 | Home Screen
251 | {
253 | navigation.navigate('ProfileScreen', { name: 'Jane' });
254 | }}
255 | title="Go to Jane's profile"
256 | />
257 | {
259 | navigation.navigate('Examples');
260 | }}
261 | title="Exit"
262 | />
263 |
264 | );
265 | }
266 | }
267 |
268 | class ProfileScreen extends React.Component {
269 | static navigationOptions = GestureTransition.navigationOptions;
270 | render() {
271 | const { navigation } = this.props;
272 | return (
273 |
274 |
275 |
276 | {navigation.getParam('name')}
277 | 's Profile
278 |
279 | navigation.goBack()} title="Go Back" />
280 |
281 |
282 | );
283 | }
284 | }
285 |
286 | const App = createNavigator(
287 | Transitioner,
288 | StackRouter({
289 | HomeScreen,
290 | ProfileScreen,
291 | }),
292 | {}
293 | );
294 |
295 | export default App;
296 |
--------------------------------------------------------------------------------
/example/src/examples/Modal.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Button,
4 | Text as UnstyledText,
5 | View as UnstyledView,
6 | StyleSheet,
7 | TouchableWithoutFeedback,
8 | } from 'react-native';
9 | import { createNavigator, StackRouter } from '@react-navigation/core';
10 | import { Transitioner } from 'react-navigation-transitioner';
11 | import Animated, { Easing } from 'react-native-reanimated';
12 | const { Value, timing, interpolate } = Animated;
13 |
14 | class FadeTransition extends React.Component {
15 | static navigationOptions = {
16 | createTransition: transition => ({
17 | ...transition,
18 | progress: new Value(0),
19 | }),
20 | runTransition: transition =>
21 | new Promise(resolve => {
22 | timing(transition.progress, {
23 | toValue: 1,
24 | duration: 500,
25 | easing: Easing.inOut(Easing.cubic),
26 | }).start(resolve);
27 | }),
28 | };
29 | render() {
30 | const {
31 | transition,
32 | navigation,
33 | transitioningFromState,
34 | transitioningToState,
35 | transitionRouteKey,
36 | } = this.props;
37 | const myKey = navigation.state.key;
38 | let opacity = 1;
39 | let transform = [];
40 | if (transitionRouteKey && transition) {
41 | const { progress } = transition;
42 | const wasVisible = !!transitioningFromState.routes.find(
43 | r => r.key === myKey
44 | );
45 | const isVisible = !!transitioningToState.routes.find(
46 | r => r.key === myKey
47 | );
48 | const fromOpacity = wasVisible ? 1 : 0;
49 | const toOpacity = isVisible ? 1 : 0;
50 |
51 | const fromTranslate = wasVisible ? 0 : 300;
52 | const toTranslate = isVisible ? 0 : 300;
53 | opacity = interpolate(progress, {
54 | inputRange: [0, 1],
55 | outputRange: [fromOpacity, toOpacity],
56 | });
57 | transform = [
58 | {
59 | translateY: interpolate(progress, {
60 | inputRange: [0, 1],
61 | outputRange: [fromTranslate, toTranslate],
62 | }),
63 | },
64 | ];
65 | }
66 | return (
67 |
68 | navigation.goBack()}>
69 |
75 |
76 |
86 | {this.props.children}
87 |
88 |
89 | );
90 | }
91 | }
92 |
93 | const View = props => (
94 |
98 | );
99 | const Text = props => (
100 |
101 | );
102 |
103 | class HomeScreen extends React.Component {
104 | render() {
105 | const { navigation } = this.props;
106 | return (
107 |
108 | Home Screen
109 | {
111 | navigation.navigate('ProfileScreen', { name: 'Jane' });
112 | }}
113 | title="Go to Jane's profile"
114 | />
115 | {
117 | navigation.navigate('Examples');
118 | }}
119 | title="Exit"
120 | />
121 |
122 | );
123 | }
124 | }
125 |
126 | class ProfileScreen extends React.Component {
127 | static navigationOptions = FadeTransition.navigationOptions;
128 | render() {
129 | const { navigation } = this.props;
130 | return (
131 |
132 |
133 |
134 | {navigation.getParam('name')}
135 | 's Profile
136 |
137 | navigation.goBack()} title="Go Back" />
138 |
139 |
140 | );
141 | }
142 | }
143 |
144 | const App = createNavigator(
145 | Transitioner,
146 | StackRouter({
147 | HomeScreen,
148 | ProfileScreen,
149 | }),
150 | {}
151 | );
152 |
153 | export default App;
154 |
--------------------------------------------------------------------------------
/example/src/examples/SharedEl.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Text,
4 | Button,
5 | View,
6 | TouchableWithoutFeedback,
7 | Image,
8 | SafeAreaView,
9 | ScrollView,
10 | } from 'react-native';
11 | import {
12 | Shared,
13 | createStackTransitionNavigator,
14 | } from 'react-navigation-transitioner';
15 |
16 | const { SharedView, SharedText, SharedFadeTransition } = Shared;
17 |
18 | const PRODUCTS = {
19 | A: {
20 | image: {
21 | uri: 'https://www.organicfacts.net/wp-content/uploads/blueberries.jpg',
22 | },
23 | name: 'Blueberries',
24 | },
25 | B: {
26 | image: {
27 | uri:
28 | 'https://www.organicfacts.net/wp-content/uploads/sugarinstrawberries.jpg',
29 | },
30 | name: 'Strawberries',
31 | },
32 | C: {
33 | image: {
34 | uri:
35 | 'https://www.organicfacts.net/wp-content/uploads/pineapplecalories.jpg',
36 | },
37 | name: 'Pineapple',
38 | },
39 | };
40 |
41 | const ProductPhoto = ({ onPress, style, id }) => {
42 | const i = ;
43 | if (onPress) {
44 | return (
45 |
46 |
47 |
48 | {i}
49 |
50 |
51 |
52 | );
53 | } else {
54 | return (
55 |
56 | {i}
57 |
58 | );
59 | }
60 | };
61 |
62 | class Home extends React.Component {
63 | static navigationOptions = SharedFadeTransition.navigationOptions;
64 | render() {
65 | return (
66 |
67 |
68 | {Object.keys(PRODUCTS).map(id => (
69 |
70 |
71 | {
75 | this.props.navigation.navigate({
76 | routeName: 'Product',
77 | params: { id },
78 | key: id,
79 | });
80 | }}
81 | />
82 |
83 |
84 | {PRODUCTS[id].name}
85 |
86 |
87 |
88 |
89 | ))}
90 |
91 |
92 | );
93 | }
94 | }
95 |
96 | const RNTitle = ({ children }) => (
97 |
98 | {children}
99 |
100 | );
101 | const RNTextBody = ({ children }) => (
102 | {children}
103 | );
104 | const RNText = () => (
105 |
106 | Build native mobile apps using JavaScript and React:
107 |
108 | React Native lets you build mobile apps using only JavaScript. It uses the
109 | same design as React, letting you compose a rich mobile UI from
110 | declarative components.
111 |
112 | A React Native app is a real mobile app:
113 |
114 | With React Native, you don't build a "mobile web app", an "HTML5 app", or
115 | a "hybrid app". You build a real mobile app that's indistinguishable from
116 | an app built using Objective-C, Java, or Swift. React Native uses the same
117 | fundamental UI building blocks as regular iOS and Android apps. You just
118 | put those building blocks together using JavaScript and React.
119 |
120 | Don't waste time recompiling:
121 |
122 | React Native lets you build your app faster. Instead of recompiling, you
123 | can reload your app instantly. With hot reloading, you can even run new
124 | code while retaining your application state. Give it a try - it's a
125 | magical experience.
126 |
127 | Use native code when you need to:
128 |
129 | React Native combines smoothly with components written in Objective-C,
130 | Java, or Swift. It's simple to drop down to native code if you need to
131 | optimize a few aspects of your application. It's also easy to build part
132 | of your app in React Native, and part of your app using native code
133 | directly - that's how the Facebook app works.
134 |
135 |
136 | );
137 |
138 | class Product extends React.Component {
139 | static navigationOptions = SharedFadeTransition.navigationOptions;
140 |
141 | render() {
142 | const { navigation } = this.props;
143 | const product = PRODUCTS[navigation.getParam('id')];
144 | return (
145 |
146 |
147 |
151 |
160 |
165 | {product.name}
166 |
167 |
168 |
169 | {
171 | navigation.goBack();
172 | }}
173 | title="Go back"
174 | />
175 |
176 |
177 | {Object.keys(PRODUCTS).map(id => {
178 | if (id === navigation.getParam('id')) {
179 | return null;
180 | }
181 | return (
182 | {
186 | navigation.navigate({
187 | routeName: 'Product',
188 | params: { id },
189 | key: id,
190 | });
191 | }}
192 | style={{ width: 80, aspectRatio: 1 }}
193 | />
194 | );
195 | })}
196 |
197 |
198 |
199 |
200 |
201 |
202 | );
203 | }
204 | }
205 |
206 | const App = createStackTransitionNavigator({
207 | Home,
208 | Product,
209 | });
210 |
211 | export default App;
212 |
--------------------------------------------------------------------------------
/jest-setup.js:
--------------------------------------------------------------------------------
1 | /**
2 | * eslint-env jest
3 | */
4 |
5 | // No setup
6 |
7 | import React from 'react';
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-navigation-transitioner",
3 | "version": "1.0.0-alpha.3",
4 | "description": "View Transitioning library for React Native and React Navigation.",
5 | "main": "dist/index.js",
6 | "files": [
7 | "dist/",
8 | "src/",
9 | "LICENSE.md",
10 | "README.md"
11 | ],
12 | "react-native": "src/index.js",
13 | "scripts": {
14 | "test": "jest",
15 | "lint": "eslint .",
16 | "format": "eslint . --fix",
17 | "build": "babel --no-babelrc --plugins=transform-react-jsx,transform-class-properties,transform-object-rest-spread,transform-flow-strip-types src --copy-files --out-dir dist --ignore '**/__tests__/**'",
18 | "prepare": "yarn build",
19 | "release": "release-it"
20 | },
21 | "publishConfig": {
22 | "registry": "https://registry.npmjs.org/"
23 | },
24 | "keywords": [
25 | "react-native-component",
26 | "react-component",
27 | "react-native",
28 | "react-navigation",
29 | "ios",
30 | "android",
31 | "transitioner"
32 | ],
33 | "repository": {
34 | "type": "git",
35 | "url": "git+https://github.com/react-navigation/react-navigation-transitioner.git"
36 | },
37 | "author": "",
38 | "license": "MIT",
39 | "bugs": {
40 | "url": "https://github.com/react-navigation/react-navigation-transitioner/issues"
41 | },
42 | "homepage": "https://github.com/react-navigation/react-navigation-transitioner#readme",
43 | "dependencies": {
44 | "react-native-reanimated": "^1.0.0-alpha.10"
45 | },
46 | "devDependencies": {
47 | "babel-cli": "^6.26.0",
48 | "babel-jest": "^22.4.1",
49 | "babel-plugin-transform-class-properties": "^6.13.0",
50 | "babel-plugin-transform-flow-strip-types": "^6.22.0",
51 | "babel-plugin-transform-object-rest-spread": "^6.13.0",
52 | "babel-plugin-transform-react-jsx": "^6.18.0",
53 | "babel-preset-react-native": "^4.0.0",
54 | "conventional-changelog-cli": "^2.0.5",
55 | "eslint": "^4.12.1",
56 | "eslint-config-satya164": "^1.0.1",
57 | "eslint-plugin-react-native-globals": "^0.1.0",
58 | "husky": "^0.14.3",
59 | "jest": "^22.1.3",
60 | "prettier": "^1.8.2",
61 | "react": "16.3.1",
62 | "react-dom": "16.3.1",
63 | "react-native": "~0.55.4",
64 | "react-test-renderer": "16.3.1",
65 | "release-it": "^7.6.1"
66 | },
67 | "peerDependencies": {
68 | "@react-navigation/core": "*",
69 | "@react-navigation/native": "*",
70 | "react": "*",
71 | "react-native": "*"
72 | },
73 | "jest": {
74 | "preset": "react-native",
75 | "testRegex": "/__tests__/[^/]+-test\\.js$",
76 | "setupFiles": [
77 | "/jest-setup.js"
78 | ],
79 | "coveragePathIgnorePatterns": [
80 | "jest-setup.js"
81 | ],
82 | "modulePathIgnorePatterns": [
83 | "/example/"
84 | ],
85 | "transformIgnorePatterns": [
86 | "node_modules/(?!(jest-)?react-native|react-clone-referenced-element|react-navigation-deprecated-tab-navigator|react-navigation)"
87 | ]
88 | },
89 | "prettier": {
90 | "trailingComma": "es5",
91 | "singleQuote": true
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/Shared.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Animated, { Easing } from 'react-native-reanimated';
3 | import { TransitionContext } from './Transitioner';
4 |
5 | const { add, cond, and, sub, divide, interpolate, timing } = Animated;
6 |
7 | const measureEl = async sharedElement => {
8 | const layout = await new Promise((resolve, reject) => {
9 | const sharedNode = sharedElement.getNode();
10 | sharedNode.measureInWindow((x, y, w, h) => {
11 | resolve({ x: x, y: y, w, h });
12 | }, reject);
13 | });
14 | return layout;
15 | };
16 |
17 | const setAnimatedValueOnKey = (obj, key, value) => {
18 | const val = obj[key] || (obj[key] = new Animated.Value(value));
19 | val.setValue(value);
20 | };
21 |
22 | const setLayoutOnKey = (obj, key, layout) => {
23 | const layoutObj = obj[key] || (obj[key] = {});
24 | setAnimatedValueOnKey(layoutObj, 'w', layout.w);
25 | setAnimatedValueOnKey(layoutObj, 'h', layout.h);
26 | setAnimatedValueOnKey(layoutObj, 'x', layout.x);
27 | setAnimatedValueOnKey(layoutObj, 'y', layout.y);
28 | setAnimatedValueOnKey(layoutObj, 'hasMeasured', 1);
29 | };
30 |
31 | const getLayout = (layoutsObj, id) => {
32 | if (!layoutsObj) {
33 | return null;
34 | }
35 | const layout =
36 | layoutsObj[id] ||
37 | (layoutsObj[id] = {
38 | hasMeasured: new Animated.Value(0),
39 | x: new Animated.Value(0),
40 | y: new Animated.Value(0),
41 | w: new Animated.Value(0),
42 | h: new Animated.Value(0),
43 | });
44 | return layout;
45 | };
46 |
47 | const createSharedTransition = transition => {
48 | const progress = new Animated.Value(0);
49 |
50 | return {
51 | ...transition,
52 | progress,
53 | fromLayouts: {},
54 | transitionLayouts: {},
55 | transitionScreenLayout: {},
56 | fromScreenLayout: {},
57 | };
58 | };
59 |
60 | const runSharedTransition = async (
61 | transition,
62 | transitionScreenRefs,
63 | fromState,
64 | toState
65 | ) => {
66 | // By now, everything is already rendered. This is our opportunity to measure shared
67 | // elements and set those measurements into Animated values so that the pre-rendered
68 | // transition looks correct
69 |
70 | const transitionRouteKey = transition.transitionRouteKey;
71 | const fromRouteKey = transition.fromRouteKey;
72 | const fromScreen = transitionScreenRefs[fromRouteKey].current;
73 | const transitionScreen = transitionScreenRefs[transitionRouteKey].current;
74 | const fromSharedElements = (fromScreen && fromScreen.sharedElements) || {};
75 | const toSharedElements =
76 | (transitionScreen && transitionScreen.sharedElements) || {};
77 | const sharedElementIds = Object.keys(fromSharedElements).filter(
78 | i => Object.keys(toSharedElements).indexOf(i) !== -1
79 | );
80 | const fromLayouts = await Promise.all(
81 | sharedElementIds.map(async id => {
82 | const element = fromSharedElements[id];
83 | return await measureEl(element);
84 | })
85 | ); // todo, collapse these into one parallel promise.all:
86 | const transitionLayouts = await Promise.all(
87 | sharedElementIds.map(async id => {
88 | const element = toSharedElements[id];
89 | return await measureEl(element);
90 | })
91 | );
92 | const transitionScreenLayout = await measureEl(transitionScreen.getEl());
93 | const fromScreenLayout = await measureEl(fromScreen.getEl());
94 |
95 | setLayoutOnKey(transition, 'transitionScreenLayout', transitionScreenLayout);
96 | setLayoutOnKey(transition, 'fromScreenLayout', fromScreenLayout);
97 |
98 | sharedElementIds.forEach((sharedElId, index) => {
99 | setLayoutOnKey(
100 | transition.transitionLayouts,
101 | sharedElId,
102 | transitionLayouts[index]
103 | );
104 | setLayoutOnKey(transition.fromLayouts, sharedElId, fromLayouts[index]);
105 | });
106 | const destValue = toState.routes.find(
107 | r => r.key === transition.transitionRouteKey
108 | )
109 | ? 1
110 | : 0;
111 | await new Promise(resolve => {
112 | timing(transition.progress, {
113 | easing: Easing.out(Easing.cubic),
114 | duration: 600,
115 | toValue: destValue,
116 | useNativeDriver: true,
117 | }).start(resolve);
118 | });
119 | };
120 |
121 | const SharedScreenContext = React.createContext(null);
122 |
123 | export class SharedFadeTransition extends React.Component {
124 | static navigationOptions = {
125 | createTransition: createSharedTransition,
126 | runTransition: runSharedTransition,
127 | };
128 |
129 | sharedElements = {};
130 | _screenEl = React.createRef();
131 |
132 | getEl = () => {
133 | return this._screenEl.current;
134 | };
135 |
136 | _setSharedElement = (id, ref) => {
137 | this.sharedElements[id] = ref;
138 | };
139 | _sharedScreenContext = {
140 | setSharedElement: this._setSharedElement,
141 | getNavigation: () => this.props.navigation,
142 | getTransitioningFromState: () => this.props.transitioningFromState,
143 | getTransitioningToState: () => this.props.transitioningToState,
144 | };
145 | render() {
146 | const { transition, children } = this.props;
147 | let opacity = 1;
148 | if (transition) {
149 | const { progress } = transition;
150 | opacity = interpolate(progress, {
151 | inputRange: [0, 1],
152 | outputRange: [0, 1],
153 | });
154 | }
155 | return (
156 |
157 |
165 | {children}
166 |
167 |
168 | );
169 | }
170 | }
171 |
172 | const getTransitionElementStyle = (transitionContext, screenContext, id) => {
173 | const transition = transitionContext.getTransition();
174 | const thisScreenKey = screenContext.getNavigation().state.key;
175 | if (!transition) {
176 | return [{ transform: [] }];
177 | }
178 | const transitionLayout = getLayout(transition.transitionLayouts, id);
179 | const fromLayout = getLayout(transition.fromLayouts, id);
180 |
181 | if (!transitionLayout || !fromLayout) {
182 | throw new Error('This is unexpected');
183 | }
184 | const transitionRouteKey = transition.transitionRouteKey;
185 | const fromRouteKey = transition.fromRouteKey;
186 | const isTransitionScreen = transitionRouteKey === thisScreenKey;
187 | const isFromScreen = fromRouteKey === thisScreenKey;
188 |
189 | const isMeasured = and(transitionLayout.hasMeasured, fromLayout.hasMeasured);
190 |
191 | const doInterpolate = (measureVal, start, end) =>
192 | interpolate(transition.progress, {
193 | inputRange: [0, Number.EPSILON, 1],
194 | outputRange: [measureVal, start, end],
195 | });
196 |
197 | const interpolateScale = (to, from) => {
198 | if (isTransitionScreen) {
199 | return doInterpolate(1, divide(from, to), 1);
200 | } else if (isFromScreen) {
201 | return doInterpolate(1, 1, divide(to, from));
202 | } else {
203 | return doInterpolate(1, 1, 1);
204 | }
205 | };
206 | const interpolateTranslate = (toOffset, fromOffset, toScale, fromScale) => {
207 | if (isTransitionScreen) {
208 | return doInterpolate(
209 | 0,
210 | sub(
211 | add(fromOffset, divide(fromScale, 2)),
212 | add(toOffset, divide(toScale, 2))
213 | ),
214 | 0
215 | );
216 | } else if (isFromScreen) {
217 | return doInterpolate(
218 | 0,
219 | 0,
220 | sub(
221 | add(toOffset, divide(toScale, 2)),
222 | add(fromOffset, divide(fromScale, 2))
223 | )
224 | );
225 | } else {
226 | return doInterpolate(0, 0, 0);
227 | }
228 | };
229 |
230 | const scaleTransform = (to, from) =>
231 | cond(isMeasured, interpolateScale(to, from), 1);
232 | const translateTransform = (toOffset, fromOffset, toScale, fromScale) =>
233 | cond(
234 | isMeasured,
235 | interpolateTranslate(toOffset, fromOffset, toScale, fromScale),
236 | 0
237 | );
238 |
239 | return {
240 | transform: [
241 | {
242 | translateX: translateTransform(
243 | transitionLayout.x,
244 | fromLayout.x,
245 | transitionLayout.w,
246 | fromLayout.w
247 | ),
248 | },
249 | {
250 | translateY: translateTransform(
251 | transitionLayout.y,
252 | fromLayout.y,
253 | transitionLayout.h,
254 | fromLayout.h
255 | ),
256 | },
257 | {
258 | scaleX: scaleTransform(transitionLayout.w, fromLayout.w),
259 | },
260 | {
261 | scaleY: scaleTransform(transitionLayout.h, fromLayout.h),
262 | },
263 | ],
264 | };
265 | };
266 |
267 | class SharedViewWithContext extends React.Component {
268 | render() {
269 | const {
270 | sharedScreenContext,
271 | transitionContext,
272 | id,
273 | style,
274 | children,
275 | } = this.props;
276 |
277 | const sharedElId = `${id}_view`;
278 |
279 | if (!sharedScreenContext) {
280 | throw new Error('Cannot render shared element outside of shared screen!');
281 | }
282 | const { setSharedElement } = sharedScreenContext;
283 |
284 | if (!transitionContext) {
285 | throw new Error('Cannot render shared element outside of transitioner!');
286 | }
287 |
288 | return (
289 | setSharedElement(sharedElId, r)}
299 | >
300 | {children}
301 |
302 | );
303 | }
304 | }
305 |
306 | export const SharedView = props => (
307 |
308 | {transitionContext => (
309 |
310 | {sharedScreenContext => (
311 |
316 | )}
317 |
318 | )}
319 |
320 | );
321 |
322 | class SharedTextWithContext extends React.Component {
323 | render() {
324 | const {
325 | sharedScreenContext,
326 | transitionContext,
327 | id,
328 | style,
329 | children,
330 | fontSize,
331 | color,
332 | } = this.props;
333 |
334 | const sharedElId = `${id}_text`;
335 |
336 | if (!sharedScreenContext) {
337 | throw new Error('Cannot render shared element outside of shared screen!');
338 | }
339 | const { setSharedElement } = sharedScreenContext;
340 |
341 | if (!transitionContext) {
342 | throw new Error('Cannot render shared element outside of transitioner!');
343 | }
344 |
345 | return (
346 | setSharedElement(sharedElId, r)}
357 | >
358 | {children}
359 |
360 | );
361 | }
362 | }
363 |
364 | export const SharedText = props => (
365 |
366 | {transitionContext => (
367 |
368 | {sharedScreenContext => (
369 |
374 | )}
375 |
376 | )}
377 |
378 | );
379 |
--------------------------------------------------------------------------------
/src/Transitioner.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet } from 'react-native';
3 | import Animated from 'react-native-reanimated';
4 | import { NavigationProvider } from '@react-navigation/core';
5 |
6 | export const TransitionContext = React.createContext(null);
7 |
8 | const interleaveArrays = (a, b) => {
9 | let aIndex = 0;
10 | let bIndex = 0;
11 | let out = [];
12 | const ensureItem = item => {
13 | if (item && out.indexOf(item) === -1) {
14 | out.push(item);
15 | }
16 | };
17 | while (aIndex <= a.length - 1 || aIndex <= b.length - 1) {
18 | ensureItem(a[aIndex]);
19 | aIndex += 1;
20 | ensureItem(b[bIndex]);
21 | bIndex += 1;
22 | }
23 | return out;
24 | };
25 |
26 | const getTransitionOwnerRouteKey = (fromState, toState) => {
27 | let transitionKey = fromState.routes[fromState.index].key;
28 | if (toState && toState.index >= fromState.index) {
29 | transitionKey = toState.routes[toState.index].key;
30 | }
31 | return transitionKey;
32 | };
33 |
34 | const defaultCreateTransition = transition => {
35 | return { ...transition };
36 | };
37 |
38 | const defaultRunTransition = () => {};
39 |
40 | const defaultRenderScreen = (
41 | ScreenComponent, transition, transitions, transitioningFromState,
42 | transitioningToState, transitionRouteKey, navigation, ref, behindScreenStyles,
43 | ) => (
44 |
48 |
57 |
58 | );
59 |
60 | const defaultRenderContainer = (transitionRouteKey, transitions, navigation,
61 | transitioningFromState, transitioningToState, transitionRefs, children) => (
62 | {children}
63 | );
64 |
65 | const getStateForNavChange = (props, state) => {
66 | // by this point we know the nav state has changed and it is safe to provide a new state. static
67 | // getDerivedStateFromProps will never interrupt a transition (when there is state.transitionRouteKey),
68 | // and _runTransition runs this after the previous transition is complete.
69 | const { navigation } = props;
70 | const nextNavState = navigation.state;
71 |
72 | // Transitions are requested by setting nav state.isTransitioning to true.
73 | // If false, we set the state immediately without transition
74 | if (!nextNavState.isTransitioning) {
75 | return {
76 | transitions: state.transitions,
77 | transitionRouteKey: null,
78 | transitioningFromState: null,
79 | transitioningFromDescriptors: null,
80 | navState: nextNavState,
81 | descriptors: props.descriptors,
82 | };
83 | }
84 | const transitionRouteKey = getTransitionOwnerRouteKey(
85 | state.navState,
86 | nextNavState
87 | );
88 | const descriptor =
89 | props.descriptors[transitionRouteKey] ||
90 | state.descriptors[transitionRouteKey] ||
91 | state.transitioningFromDescriptors[transitionRouteKey];
92 | const { options } = descriptor;
93 | const fromRoute = state.navState.routes[state.navState.index];
94 | const createTransition = options.createTransition || defaultCreateTransition;
95 | const transition =
96 | state.transitions[transitionRouteKey] ||
97 | createTransition({
98 | navigation: props.navigation,
99 | transitionRouteKey,
100 | fromRouteKey: fromRoute.key,
101 | });
102 | return {
103 | transitions: {
104 | ...state.transitions,
105 | [transitionRouteKey]: transition,
106 | },
107 | transitionRouteKey,
108 | transitioningFromState: state.navState,
109 | transitioningFromDescriptors: state.descriptors,
110 | navState: nextNavState,
111 | descriptors: props.descriptors,
112 | };
113 | };
114 |
115 | export class Transitioner extends React.Component {
116 | state = {
117 | // an object of transitions by route key
118 | transitions: {},
119 | // if this is present, there is a transition in progress:
120 | transitionRouteKey: null,
121 | // this will be the previous nav state and descriptors, when there is a transition in progress
122 | transitioningFromState: null,
123 | transitioningFromDescriptors: {},
124 | // this is the current navigation state and descriptors:
125 | navState: this.props.navigation.state,
126 | descriptors: this.props.descriptors,
127 | };
128 |
129 | // never re-assign this!
130 | _transitionRefs = {};
131 |
132 | static getDerivedStateFromProps = (props, state) => {
133 | // Transition only happens when nav state changes
134 | if (props.navigation.state === state.navState) {
135 | return state;
136 | }
137 | // Never interrupt a transition in progress.
138 | if (state.transitionRouteKey) {
139 | return state;
140 | }
141 | return getStateForNavChange(props, state);
142 | };
143 |
144 | async _startTransition() {
145 | // Put state in function scope, so we are confident that we refer to the exact same state later for getStateForNavChange.
146 | // Even though our state shouldn't change during the animation.
147 | const { state } = this;
148 | const {
149 | transitions,
150 | transitionRouteKey,
151 | transitioningFromState,
152 | transitioningFromDescriptors,
153 | navState,
154 | descriptors,
155 | } = state;
156 |
157 | const descriptor =
158 | descriptors[transitionRouteKey] ||
159 | transitioningFromDescriptors[transitionRouteKey];
160 | const { runTransition } = descriptor.options;
161 | const run = runTransition || defaultRunTransition;
162 |
163 | const transition = transitions[transitionRouteKey];
164 | // Run animation, this might take some time..
165 | await run(
166 | transition,
167 | this._transitionRefs,
168 | transitioningFromState,
169 | navState
170 | );
171 |
172 | // after async animator, this.props may have changed. re-check it now:
173 | if (navState === this.props.navigation.state) {
174 | // Navigation state is currently the exact state we were transitioning to. Set final state and we're done
175 | const transitions = {}; // clear out unusued transitions
176 | navState.routes.map(r => r.key).forEach(activeRouteKey => {
177 | transitions[activeRouteKey] = state.transitions[activeRouteKey];
178 | });
179 | this.setState({
180 | transitions,
181 | transitionRouteKey: null,
182 | transitioningFromState: null,
183 | transitioningFromDescriptors: {},
184 | // navState and descriptors remain unchanged at this point.
185 | });
186 | // Also de-reference old screenRefs by replacing this._transitionRefs
187 | const toKeySet = new Set(navState.routes.map(r => r.key));
188 | navState.routes.forEach(r => {
189 | if (!toKeySet.has(r.key)) {
190 | delete this._transitionRefs[r.key];
191 | }
192 | });
193 | } else {
194 | // Navigation state prop has changed during the transtion! Schedule another transition
195 | this.setState(getStateForNavChange(this.props, state));
196 | }
197 | }
198 |
199 | componentDidUpdate(lastProps, lastState) {
200 | if (
201 | // If we are transitioning
202 | this.state.transitionRouteKey &&
203 | // And this is a new transition,
204 | lastState.transitioningFromState !== this.state.transitioningFromState
205 | ) {
206 | this._startTransition().then(
207 | () => {},
208 | e => {
209 | console.error('Error running transition:', e);
210 | }
211 | );
212 | }
213 | }
214 |
215 | _transitionContext = {
216 | getTransition: transitionRouteKey => {
217 | const { navState, transitionFromState, transitions } = this.state;
218 | const defaultTransitionKey = getTransitionOwnerRouteKey(
219 | navState,
220 | transitionFromState
221 | );
222 | const key = transitionRouteKey || defaultTransitionKey;
223 | return transitions[key];
224 | },
225 | };
226 |
227 | render() {
228 | const {
229 | transitions,
230 | transitionRouteKey,
231 | transitioningFromState,
232 | transitioningFromDescriptors,
233 | navState,
234 | descriptors,
235 | } = this.state;
236 | const { navigation } = this.props;
237 | const mainRouteKeys = navState.routes.map(r => r.key);
238 | let routeKeys = mainRouteKeys;
239 |
240 | if (transitionRouteKey) {
241 | if (transitioningFromState) {
242 | const prevRouteKeys = transitioningFromState.routes.map(r => r.key);
243 | // While transitioning, our main nav state is navState. But we also need to render screens from the last state, preserving the order
244 | routeKeys = interleaveArrays(prevRouteKeys, mainRouteKeys);
245 | }
246 | }
247 |
248 | // Use render container function from last route descriptor
249 | const renderContainerFunc = descriptors[transitionRouteKey].options.renderContainer
250 | || defaultRenderContainer;
251 |
252 | return (
253 |
254 | {renderContainerFunc(transitionRouteKey, transitions, navigation,
255 | transitioningFromState, transitionRouteKey ? navigation.state : null,
256 | routeKeys.map((key, index) => {
257 | const ref =
258 | this._transitionRefs[key] ||
259 | (this._transitionRefs[key] = React.createRef());
260 | const descriptor =
261 | descriptors[key] || transitioningFromDescriptors[key];
262 | const C = descriptor.getComponent();
263 |
264 | const aboveScreenRouteKeys = routeKeys.slice(index + 1);
265 | let behindScreenStyles = aboveScreenRouteKeys.map(
266 | aboveScreenRouteKey => {
267 | const aboveTransition = transitions[aboveScreenRouteKey];
268 | const aboveScreenDescriptor =
269 | descriptors[aboveScreenRouteKey] ||
270 | transitioningFromDescriptors[aboveScreenRouteKey];
271 | const { options } = aboveScreenDescriptor;
272 | if (
273 | !aboveTransition ||
274 | !options.getBehindTransitionAnimatedStyle
275 | ) {
276 | return {};
277 | }
278 | return options.getBehindTransitionAnimatedStyle(aboveTransition);
279 | }
280 | );
281 |
282 | let transition = transitions[key];
283 | if (behindScreenStyles.length === 0) {
284 | // bizarre react-native bug that refuses to clear Animated.View styles unless you do something like this..
285 | // to reproduce the problem, set a getBehindTransitionAnimatedStyle that puts opacity at 0.5
286 | behindScreenStyles = [{ opacity: 1 }];
287 | }
288 |
289 | const renderFunc = descriptor.options.renderScreen || defaultRenderScreen;
290 |
291 | return (
292 |
293 | {renderFunc(C, transition, transitions, transitioningFromState,
294 | transitionRouteKey ? navigation.state : null,
295 | transitionRouteKey, descriptor.navigation, ref,
296 | behindScreenStyles, key)}
297 |
298 | );
299 | }))}
300 |
301 | );
302 | }
303 | }
304 |
305 | export default Transitioner;
306 |
--------------------------------------------------------------------------------
/src/createStackTransitionNavigator.js:
--------------------------------------------------------------------------------
1 | import { createNavigator, StackRouter } from '@react-navigation/core';
2 | import Transitioner from './Transitioner';
3 |
4 | export default function createStackTransitionNavigator(
5 | routeConfigs,
6 | options = {}
7 | ) {
8 | const router = StackRouter(routeConfigs, options);
9 |
10 | return createNavigator(Transitioner, router, options);
11 | }
12 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-commonjs */
2 |
3 | module.exports = {
4 | /**
5 | * Navigators
6 | */
7 | get createStackTransitionNavigator() {
8 | return require('./createStackTransitionNavigator').default;
9 | },
10 |
11 | get Shared() {
12 | return require('./Shared');
13 | },
14 |
15 | /**
16 | * Views
17 | */
18 | get Transitioner() {
19 | return require('./Transitioner').default;
20 | },
21 | };
22 |
--------------------------------------------------------------------------------