Compare commits

..

93 Commits

Author SHA1 Message Date
Eric Vicenti
0ea01673e5 transitioner2
Introducing a wild and hacky prototype of Transitioner 2!

- "transitionProps" concept is now identical to transitioner.state
- Far simpler state management than before
- Descriptors only, no "scenes"
- No more "position"

Also, StackView is seeing improvements:
- Easier to understand when "transition props" are just this.props

- StackView only renders the current sceen and the one before it

Currently broken:
- Interpolation configuration, beyond the first push
- Attempting to move away from getSceneIndicesForInterpolationInputRange because it works with position and it is confusing as hell, but haven't worked around it yet
- Gestures, although some "backProgress" code is in the works
- Interpolation is super buggy
- onTransitionStart/End
- Header, although a lot of code is moved over
2018-03-11 00:11:58 -08:00
Brent Vatne
71adb7cc4f Update DrawerView.js 2018-03-06 14:22:31 -08:00
Eric Vicenti
77343cb096 Merge branch 'master' into @ericvicenti/drawer-fix 2018-03-06 11:43:25 -08:00
corupta
7c488c8d49 Add activeLabelStyle and inactiveLabelStyle for DrawerItem (#3559)
* Add activeLabelStyle and inactiveLabelStyle

Adding activeLabelStyle, so that active items can be customized more.
My use case: Change font of the active item to bold.
Also, added inactiveLabelStyle which can be used for a similar purpose.

* prettier fix

* Update react-navigation.js

* prettier fix

* Update jest snapshot for DrawerNavigator - for adding a new style property to the styles array
2018-03-05 17:17:08 -08:00
Edward Drapkin
9005494e64 More specific injected Navigation props (#3645) 2018-03-05 16:33:42 -08:00
Brent Vatne
accee76951 Merge branch 'master' into @ericvicenti/drawer-fix 2018-03-05 16:23:10 -08:00
Brent Vatne
0daab8c55b Add isFocused helper to navigation and fix withNavigationFocus 2018-03-05 12:28:42 -08:00
Eric Vicenti
bbb8c4d8d3 Fix issue in drawer actions 2018-03-05 11:38:13 -08:00
Brent Vatne
ae98089337 Cache event subscribers and clean up after unmounting (#3648)
* This caches "child event subscribers" by key and removes them when they're not needed anymore. Once a navigator unmounts we also remove upstream subscribers.

* Fix tests
2018-03-05 11:27:57 -08:00
Yordis Prieto
57e37a8783 Fix typespec of back action creator (#3659) 2018-03-05 11:10:39 -08:00
Ashoat Tevosyan
b7994d28db Fix NavigationEventPayload.lastState type (#3664)
Should be nullable, since it is initially called as `null` in `src/createNavigationContainer.js` (and in `react-navigation-redux-helpers`, where it is causing a Flow error)
2018-03-05 11:04:29 -08:00
Vishwesh Jainkuniya
f1bfdeee46 Fix: tabBar icons are not visible. (#3650)
* Fix: tabBar icons are not visible.

* Fix: tests.
2018-03-04 21:21:54 -08:00
Nicolas Beck
c6301abaed Add initialRouteKey for StackRouter (#3540)
* use initialRouteName as key when initializing StackRouter

* fix null headerLeft

* merge back

* fixed tests

* use config flag

* fixed snapshots

* implemented requested changes
2018-03-02 16:06:27 -08:00
Brent Vatne
4569ad49f9 Fix regression in error message for route config validation 2018-03-02 12:18:11 -08:00
Arseny Yankovsky
214eeb13fb Allow modification of SafeAreaView props (#3496)
* SafeAreaView fix

* Updated to only allow modification of forceInset property of SafeAreaView
2018-03-01 13:42:19 -08:00
Michał Pierzchała
416fe58cab Move contributing guide to CONTRIBUTING.md (#3631) 2018-03-01 10:11:30 -08:00
Eric Vicenti
2e47cbb3cb Drawer Router (#3618) 2018-02-27 18:34:05 -08:00
Brent Vatne
cd99dc8054 Update snapshots 2018-02-27 17:32:13 -08:00
Eric Vicenti
e27ad22c57 [BREAKING] New createNavigator API (#3392)
* New createNavigator and View API

See the RFC here:
https://github.com/react-navigation/rfcs/blob/master/text/0002-navigator-view-api.md

* shattered dreams of flow

* fix export

* Fix tab view issues found by brent
2018-02-27 17:27:58 -08:00
Brent Vatne
6785729fb5 Fix snapshots 2018-02-26 16:02:38 -08:00
Brent Vatne
d3b6e70d16 Clarify that people should not report Redux or MobX related integration issues here 2018-02-26 15:53:14 -08:00
Brent Vatne
5f32a48c16 Release 1.2.1 2018-02-26 14:04:44 -08:00
Brent Vatne
d5a0b5912b Apply StyleSheet.flatten properly to headerStyle. Fixes #3608 2018-02-26 14:04:36 -08:00
Yordis Prieto
13fd8acb20 fix typespec (#3583) 2018-02-26 12:25:13 -08:00
Serhii Palash
8fc8f3b8f7 Fix bug #3279 “Action within RESET doesn't trigger” (#3593)
https://github.com/react-navigation/react-navigation/issues/3279
2018-02-26 12:14:29 -08:00
Brent Vatne
a37473c5e4 Release 1.2.0 2018-02-25 17:46:44 -08:00
Brent Vatne
23eb0839cb Improve crossfade configuration for header 2018-02-25 17:44:16 -08:00
Brent Vatne
e84473db78 Use a closer-to-correct spring animation when releasing a gesture in CardStack (#3601) 2018-02-25 17:20:39 -08:00
Brent Vatne
c8531d0f38 Pop to top on TabNavigator when tapping already focused tab icon (#3589)
* Pop to top when tapping the tab icon for an already focused tab

* Dispatch popToTop with key to scope the action properly

* Add test for POP_TO_TOP with key
2018-02-25 16:33:20 -08:00
Vojtech Novak
276fb8f7b3 remove support for deprecated navigation actions (#3595)
* remove support for deprecated navigation actions

* remove deprecated action from flow definitions
2018-02-24 12:08:14 -08:00
Johan Ruokangas
ab758bcaaa Fix RFC link (#3594) 2018-02-24 12:50:31 +01:00
Matt Hamil
2bb91a6740 Updated README (#3567)
Changed wording in the Web Support section of README to reflect priority for previous 1.0 release.
2018-02-22 21:52:09 -08:00
Brent Vatne
ffa3a92534 First crack at publish from circle (#3574)
* First crack at publish from circle

* s/publish/deploy

* Update snapshots

* Try installing with yarn

* Nice one

* Shhh

* Comment some things out and add a webhook

* Add proper webhook url and add back build steps
2018-02-22 16:39:41 -08:00
Brent Vatne
6dda8c30a9 Fix colors for card and header border on iOS 2018-02-22 12:58:33 -08:00
Brent Vatne
065fdf61d8 Several improvements to StackNavigation Header (#3568)
* Refactor to remove unused variables, styles, and outer Animated view

* Style cleanup

* Proof of concept blur background

* Clean it up and add flow interface

* Update snapshots
2018-02-21 18:29:43 -08:00
Brent Vatne
371a714b57 Release 1.1.2 2018-02-20 16:00:44 -08:00
Ron Arts
1d33b95c5f Bump react-native-safe-area-view dep to 0.7.0 for react-native-web support (#3553) 2018-02-20 15:44:20 -08:00
Mike Grabowski
593bc8a648 Improve Header performance a bit (#3556)
* Improve `Header` performance a bit

I have been investigating `<Header />` component performance once again today with `render` cycles in particular. I have observed that during `push` phase, a typical Header in NavigationPlayground re-renders 3 times on iOS and 2 times on Android.

The first time is obvious on both platforms since that's when we add a new scene to an array. Second on iOS was also self-explanatory - we measure title width and other layout parameters in order to provide a better animation. 

What got me thinking was the last render cycle that didn't have a clear origin. After digging around I've found it is caused by a `scenes` array changing when transition finishes. What is surprising, it's just the reference to the object that changes, the content (items inside) stay the same.

I have found out that this is caused by `Transitioner` looping through an array of scenes attempting to remove stale scenes. Since scene becomes stale when you `pop` from it, this is obviously a noop when `pushing` a new route. That, obviously, causes an extra render cycle since `filter` produces same, but with a different pointer, object.

* Update Transitioner.js

* Update Transitioner.js
2018-02-20 15:44:10 -08:00
Brent Vatne
174a6e4175 Release 1.1.1 2018-02-19 18:18:38 -08:00
Brent Vatne
af991e5512 Release 1.1.0 2018-02-19 18:16:23 -08:00
Brent Vatne
0b0e9e9df5 Fix TabRouter to support shorthand route config 2018-02-19 18:16:23 -08:00
Brent Vatne
42b0ccca79 Release 1.1.0-rc.5 2018-02-19 18:16:23 -08:00
Brent Vatne
da6a960bb1 Fix regression in modular back button 2018-02-19 18:16:23 -08:00
Ron Arts
3ca47ec778 Fix react-native-web support for #3526 (#3546)
* MaskedViewIOS use broke react-native-web support, this fixes it.

* Fallback more gracefully.

* Actually return the value ...
2018-02-19 13:48:12 +01:00
Marcin Raburski
14ee56a20d getComponentForState made more generic (#2498) 2018-02-19 04:14:13 -08:00
Matthieu Lemoine
9d36daf48e Pass prevTransitionsProps to transitionConfig (#3304)
* Pass prevTransitionsProps to transitionConfig

* Remove flow type
2018-02-19 02:29:46 -08:00
Brent Vatne
055fdb2ffc Release 1.1.0-rc.4 2018-02-17 12:50:26 -08:00
Brent Vatne
b3bf80634d Move the back button handler subscription to the NavigationContainer constructor (#3542)
* Move the back button handler subscription to the constructor to fix precedence
- See 07d92947a1/src/Navigation.js (L190-L193)

* Return the boolean result of dispatch in global back handler
2018-02-17 12:50:04 -08:00
Brent Vatne
fafd0e8702 Cleanup around StackRouter (#3487)
* Make StackRouter a bit easier to read, update and add some comments, add an explicit test around stack and navigate

* Address feedback for clarity
2018-02-17 11:14:03 -08:00
Brent Vatne
d94045817c Release 1.1.0-rc.3 2018-02-17 09:42:45 -08:00
Brent Vatne
f0acaddf05 Fix headerLeft on first screen regression 2018-02-17 09:40:59 -08:00
Brent Vatne
9beb32ecac Update snapshots 2018-02-16 18:14:07 -08:00
Brent Vatne
29a29936bf Remove withNavigationFocus example until it's fixed 2018-02-16 18:10:33 -08:00
Brent Vatne
8961fef00b Bump version 2018-02-16 18:09:40 -08:00
Brent Vatne
021c7e54be Do not use hide views when swipe or animations are enabled. Improve withNavigationFocus example 2018-02-16 18:05:39 -08:00
Brent Vatne
27538cad94 Properly thread through childNavigation 2018-02-16 17:26:01 -08:00
Brent Vatne
9a8a50ef1d Release 1.1.0-rc.0 2018-02-16 17:07:28 -08:00
Brent Vatne
2c6dc35723 Revert "Revert "Add getParam navigation helper (#3510)" (#3532)"
This reverts commit 5febb81a1c.
2018-02-16 17:06:16 -08:00
Brent Vatne
3600ecbd9b Add back support for lazy tabs and use removeClippedSubviews (#3538)
* Lazy initialization of tabs and move contents off-screen when not active

* Make subview clipping and lazy both configurable

* Record snapshots again

* Update type definition

* Remove unused log
2018-02-16 16:59:19 -08:00
Sébastien Lorber
c74f001b1c add withNavigationFocus HOC (#3512)
* add withNavigationFocus HOC

See:
- https://github.com/react-navigation/react-navigation/issues/51#issuecomment-276003658
- https://github.com/react-navigation/react-navigation/issues/51#issuecomment-278761705
- https://github.com/react-navigation/react-navigation/pull/3345#issuecomment-360260067

* typos

* remove unused import

* Add withNavigationFocus export

* add example TabsWithNavigationFocus

* add example TabsWithNavigationFocus

* withNavigationFocus: get navigation from context or props

* subs => subscriptions

* fix flow issues
2018-02-16 16:57:33 -08:00
Brent Vatne
7f4706e4cc Update snapshots 2018-02-16 12:45:57 -08:00
Brent Vatne
d0ef33b12f Several small changes related to header style commit 2018-02-16 12:42:44 -08:00
Brent Vatne
3c3668c952 Header transition presets with support for standard iOS transition style (#3526)
Header transition presets with approximate support for UIKit transition style
2018-02-16 12:41:59 -08:00
Brent Vatne
5febb81a1c Revert "Add getParam navigation helper (#3510)" (#3532)
This reverts commit 50dcb37cd7.
2018-02-15 13:24:40 -08:00
Peter Piekarczyk
50dcb37cd7 Add getParam navigation helper (#3510)
* add getParam helper

* passing tests

* check for existence of param instead of just or

* fix spacing

* use in instead of checking for null

* add test for null
2018-02-15 10:14:08 -08:00
Ron Arts
69bca191a7 The Linking and BackHandler components are now both supported by react-native-web (#3494)
so there's no need to supply dummy replacements.
2018-02-13 12:23:26 -08:00
Sébastien Lorber
4df9198979 fix doc link (#3513) 2018-02-13 10:30:20 -08:00
Ashoat Tevosyan
0fc7f87173 Run Prettier on Flow libdef on precommit (#3495) 2018-02-12 14:45:50 -08:00
Brent Vatne
5b4d11ab5d Better orientation layout on playground 2018-02-12 11:53:23 -08:00
Brent Vatne
4d1cc285b9 Slightly improved margins on the iOS header back button 2018-02-09 21:19:02 -08:00
Brent Vatne
41725afa8a Use the correct iOS back icon 2018-02-09 20:08:56 -08:00
Brent Vatne
58b77d44ae Make Router({ RouteName: Component }) a valid way to configure a router (#3486)
* Make Router({routeName: Component}) a valid way to instantiate a route

* Update App.js in NavigationPlayground

* Fix route config flow type
2018-02-09 18:20:01 -08:00
Kazato Sugimoto
1d49b6e3fe Decode URI encoded string of deep link (#3455) 2018-02-09 16:26:20 -08:00
Brent Vatne
58b8a08af6 Update snapshots for TabView/TabNavigator 2018-02-09 14:22:31 -08:00
Maxime Florent Fankam
b11ea8a1d5 Allow swipeEnabled be a function (#3378)
* Allow swipeEnabled be a function with state param
2018-02-09 14:04:17 -08:00
Brent Vatne
3c2b977eca Number of lines in label for TabBarBottom is always 1 2018-02-09 13:51:04 -08:00
Brent Vatne
95565487ec Make it possible to go back to menu from key example and improve banner on menu screen (#3479)
* Make it possible to go back to menu from key example and improve banner on menu screen

* Fix NavigationPlayground flow error
2018-02-08 19:10:54 -08:00
Brent Vatne
a26d2acfca Bump to patch version 1.0.3 2018-02-08 18:24:20 -08:00
Brent Vatne
a1b3d2213d No need to have a conditional around slicing the routes array 2018-02-08 18:22:02 -08:00
Brent Vatne
eb39df507e Prevent navigation from getting in bad state when navigating back to route by key (#3478) 2018-02-08 18:20:14 -08:00
Brent Vatne
cca06bb530 Do not use contentInsetAdjustmentBehavior on iOS in ModalStack example 2018-02-08 16:14:45 -08:00
Eric Vicenti
2187d8fe66 Consistent treatment of route keys (#3474)
This problem was found and fixed by @matthargett and @jayphelps in #3397. I’m just rebasing and cleaning a few things up
2018-02-08 15:28:27 -08:00
Eric Vicenti
67f98b69eb Simpler implementation of withNavigation (#3476)
This will allow for refs with onRef (fixes #3461), and will avoid all these warnings from throwing during our tests
2018-02-08 14:20:52 -08:00
Brent Vatne
c0c5c86f63 Bump to patch version 1.0.2 2018-02-08 12:49:04 -08:00
Brent Vatne
4388b6403c Remove console logs from published version of navigation playground 2018-02-08 12:48:28 -08:00
Brent Vatne
2cb740fbf6 Only initialize the CardStack PanResponder if gestures are enabled 2018-02-08 12:36:26 -08:00
Brent Vatne
ac741a703b Remove extra scene in floating header if it hasn't been evicted due to transition yet 2018-02-08 12:32:09 -08:00
Brent Vatne
5641b42975 Revert "StackRouter block actions while transitioning (#3469)"
This reverts commit 858a0d7a53.
2018-02-08 11:49:25 -08:00
Brent Vatne
ea19ceaa6a Bump to minor version 1.0.1 2018-02-08 10:47:48 -08:00
Brent Vatne
57ae6e4736 Make TabRouter handle COMPLETE_TRANSITION in a child router without switching active index (#3473) 2018-02-08 10:46:12 -08:00
Eric Vicenti
858a0d7a53 StackRouter block actions while transitioning (#3469)
The most straightforward fix for two issues is to block all navigation actions while mid-transition of a stack navigator. This will fix:

The double-navigate on double tap issue, because the first navigation will start the transition and the second action will be ignored.

Will fix the buggy header experience that you can see when going back and forward to a different route quickly. This happens because the next navigate action happens before the completion action. After the fix, the navigate action will be ignored, the user will tap again, and will see a good transition
2018-02-08 09:02:47 -08:00
Dave Pack
cf36f22e68 Sync and switch SafeAreaView with standalone (#3452)
* add react-native-safe-area-view npm package

* remove local SafeAreaView, import from package in views

* update to latest react-native-safe-area-view

* update snapshots
2018-02-07 17:32:06 -08:00
Brent Vatne
7385c244b7 Add custom back button example 2018-02-07 10:42:06 -08:00
103 changed files with 5987 additions and 2995 deletions

View File

@@ -15,9 +15,26 @@ jobs:
- v3-react-navigation-master
- run: yarn # install root deps
- run: ./scripts/test.sh # run tests
- setup_remote_docker
- deploy:
command: |
set -x
VER="17.03.0-ce"
curl -L -o /tmp/docker-$VER.tgz https://get.docker.com/builds/Linux/x86_64/docker-$VER.tgz
tar -xz -C /tmp -f /tmp/docker-$VER.tgz
mv /tmp/docker/* /usr/bin
yarn global add exp
set +x
exp login -u "$EXPO_USERNAME" -p "$EXPO_PASSWORD"
set -x
cd examples/NavigationPlayground && yarn && exp publish --release-channel "${CIRCLE_SHA1}"
- save_cache:
key: v3-react-navigation-{{ .Branch }}-{{ checksum "package.json" }}
paths:
- ~/.cache/yarn
- ~/react-navigation/examples/NavigationPlayground/node_modules
- ~/react-navigation/examples/ReduxExample/node_modules
notify:
webhooks:
- url: https://react-navigation-ci.now.sh

View File

@@ -9,6 +9,8 @@ If you have a question, feature request, or an idea for improving the library or
- [Get help on Discord chat (#react-navigation on Reactiflux)](https://discord.gg/4xEK3nD) or [on StackOverflow](https://stackoverflow.com/questions/tagged/react-navigation)
- Search for your issue - it may have already been answered or even fixed in the development branch. However, if you find that an old, closed issue still persists in the latest version, you should open a new issue.
Bugs with react-navigation must be reproducible *without any external libraries that operate on it*. This means that if you are attempting to use Redux or MobX with it and you think you have found a bug, you must be able to reproduce it without Redux or MobX in this report. Redux related issues belong in [react-navigation-redux-helpers](https://github.com/react-navigation/react-navigation-redux-helpers), and we do not have any first-class integration with MobX at the moment.
---
### Current Behavior

13
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,13 @@
# Contributing to React Navigation
This library is a community effort: it can only be great if we all help out in one way or another! If you feel like you aren't experienced enough using React Navigation to contribute, you can still make an impact by:
* Responding to one of the open [issues](https://github.com/react-community/react-navigation/issues). Even if you can't resolve or fully answer a question, asking for more information or clarity on an issue is extremely beneficial for someone to come after you to resolve the issue.
* Creating public example repositories or [Snacks](https://snack.expo.io/) of navigation problems you have solved and sharing the links in [Community Resources](https://github.com/react-navigation/react-navigation/blob/master/COMMUNITY_RESOURCES.md).
* Answering questions on [Stack Overflow](https://stackoverflow.com/search?q=react-navigation).
* Answering questions in our [Reactiflux](https://www.reactiflux.com/) channel.
* Providing feedback on the open [PRs](https://github.com/react-navigation/react-navigation/pulls).
* Providing feedback on the open [RFCs](https://github.com/react-navigation/rfcs).
* Improving the [website](https://github.com/react-navigation/react-navigation.github.io).
If you would like to submit a pull request, please follow the [Contributors guide](https://reactnavigation.org/docs/contributing.html) to find out how. If you don't know where to start, check the ones with the label [`good first issue`](https://github.com/react-community/react-navigation/labels/good%20first%20issue) - even [fixing a typo in the documentation](https://github.com/react-community/react-navigation/pull/2727) is a worthy contribution!

View File

@@ -39,17 +39,7 @@ See [the help page](https://reactnavigation.org/en/help.html).
#### How can I help?
This library is a community effort: it can only be great if we all help out in one way or another! If you feel like you aren't experienced enough using React Navigation to contribute, you can still make an impact by:
* Responding to one of the open [issues](https://github.com/react-community/react-navigation/issues). Even if you can't resolve or fully answer a question, asking for more information or clarity on an issue is extremely beneficial for someone to come after you to resolve the issue.
* Creating public example repositories or [Snacks](https://snack.expo.io/) of navigation problems you have solved and sharing the links in [Community Resources](https://github.com/react-navigation/react-navigation/blob/master/COMMUNITY_RESOURCES.md).
* Answering questions on [Stack Overflow](https://stackoverflow.com/search?q=react-navigation).
* Answering questions in our [Reactiflux](https://www.reactiflux.com/) channel.
* Providing feedback on the open [PRs](https://github.com/react-community/react-navigation/pulls).
* Providing feedback on the open [RFCs](https://github.com/react-community/rfcs/pulls).
* Improving the [website](https://github.com/react-navigation/react-navigation.github.io).
If you would like to submit a pull request, please follow the [Contributors guide](https://reactnavigation.org/docs/contributing.html) to find out how. If you don't know where to start, check the ones with the label [`good first issue`](https://github.com/react-community/react-navigation/labels/good%20first%20issue) - even [fixing a typo in the documentation](https://github.com/react-community/react-navigation/pull/2727) is a worthy contribution!
See our [Contributing Guide](CONTRIBUTING.md)!
#### Is this the only library available for navigation?
@@ -57,7 +47,7 @@ Certainly not! There other libraries - which, depending on your needs, can be be
#### Can I use this library for web?
Currently this is [not a priority](https://github.com/react-community/react-navigation/issues/2585#issuecomment-330338793), but the architecture of this library allows for it (and it has worked in the past). If you would like to lead this charge, please reach out with your ideas for how to move forward on the [RFCs repository](https://github.com/react-navigation/rfcs) and we would be happy to discuss.
Web support was [not a priority for the 1.0 release](https://github.com/react-community/react-navigation/issues/2585#issuecomment-330338793), but the architecture of this library allows for it (and it has worked in the past). If you would like to lead this charge, please reach out with your ideas for how to move forward on the [RFCs repository](https://github.com/react-navigation/rfcs) and we would be happy to discuss.
## Code of conduct

View File

@@ -6,6 +6,11 @@
"development": {
"plugins": [
"transform-react-jsx-source"
],
},
"production": {
"plugins": [
"transform-remove-console"
]
}
}

View File

@@ -65,11 +65,13 @@ module.file_ext=.jsx
module.file_ext=.json
module.file_ext=.native.js
suppress_type=$FlowIgnore
suppress_type=$FlowIssue
suppress_type=$FlowFixMe
suppress_type=$FlowFixMeProps
suppress_type=$FlowFixMeState
suppress_comment=\\(.\\|\n\\)*\\$FlowIgnore\\($\\|[^(]\\|(\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy

View File

@@ -1,11 +1,13 @@
/* @flow */
import React from 'react';
import { Constants, ScreenOrientation } from 'expo';
import { Asset, Constants, ScreenOrientation } from 'expo';
ScreenOrientation.allow(ScreenOrientation.Orientation.ALL);
import {
Animated,
Image,
Platform,
ScrollView,
StyleSheet,
@@ -16,7 +18,6 @@ import {
} from 'react-native';
import { SafeAreaView, StackNavigator } from 'react-navigation';
import Banner from './Banner';
import CustomTabs from './CustomTabs';
import CustomTransitioner from './CustomTransitioner';
import Drawer from './Drawer';
@@ -25,9 +26,13 @@ import TabsInDrawer from './TabsInDrawer';
import ModalStack from './ModalStack';
import StacksInTabs from './StacksInTabs';
import StacksOverTabs from './StacksOverTabs';
import StacksWithKeys from './StacksWithKeys';
import SimpleStack from './SimpleStack';
import StackWithHeaderPreset from './StackWithHeaderPreset';
import StackWithTranslucentHeader from './StackWithTranslucentHeader';
import SimpleTabs from './SimpleTabs';
import TabAnimations from './TabAnimations';
import TabsWithNavigationFocus from './TabsWithNavigationFocus';
const ExampleInfo = {
SimpleStack: {
@@ -42,6 +47,18 @@ const ExampleInfo = {
name: 'Drawer Example',
description: 'Android-style drawer navigation',
},
StackWithHeaderPreset: {
name: 'UIKit-style Header Transitions',
description: 'Masked back button and sliding header items. iOS only.',
},
StackWithHeaderPreset: {
name: 'UIKit-style Header Transitions',
description: 'Masked back button and sliding header items. iOS only.',
},
StackWithTranslucentHeader: {
name: 'Translucent Header',
description: 'Render arbitrary translucent content in header background.',
},
// MultipleDrawer: {
// name: 'Multiple Drawer Example',
// description: 'Add any drawer you need',
@@ -76,6 +93,10 @@ const ExampleInfo = {
name: 'Stacks over Tabs',
description: 'Nested stack navigation that pushes on top of tabs',
},
StacksWithKeys: {
name: 'Link in Stack with keys',
description: 'Use keys to link between screens',
},
LinkStack: {
name: 'Link in Stack',
description: 'Deep linking into a route in stack',
@@ -88,38 +109,28 @@ const ExampleInfo = {
name: 'Animated Tabs Example',
description: 'Tab transitions have custom animations',
},
TabsWithNavigationFocus: {
name: 'withNavigationFocus',
description: 'Receive the focus prop to know when a screen is focused',
},
};
const ExampleRoutes = {
SimpleStack: {
screen: SimpleStack,
},
SimpleTabs: {
screen: SimpleTabs,
},
Drawer: {
screen: Drawer,
},
SimpleStack: SimpleStack,
SimpleTabs: SimpleTabs,
Drawer: Drawer,
// MultipleDrawer: {
// screen: MultipleDrawer,
// },
TabsInDrawer: {
screen: TabsInDrawer,
},
CustomTabs: {
screen: CustomTabs,
},
CustomTransitioner: {
screen: CustomTransitioner,
},
ModalStack: {
screen: ModalStack,
},
StacksInTabs: {
screen: StacksInTabs,
},
StacksOverTabs: {
screen: StacksOverTabs,
},
StackWithHeaderPreset: StackWithHeaderPreset,
StackWithTranslucentHeader: StackWithTranslucentHeader,
TabsInDrawer: TabsInDrawer,
CustomTabs: CustomTabs,
CustomTransitioner: CustomTransitioner,
ModalStack: ModalStack,
StacksWithKeys: StacksWithKeys,
StacksInTabs: StacksInTabs,
StacksOverTabs: StacksOverTabs,
LinkStack: {
screen: SimpleStack,
path: 'people/Jordan',
@@ -128,48 +139,148 @@ const ExampleRoutes = {
screen: SimpleTabs,
path: 'settings',
},
TabAnimations: {
screen: TabAnimations,
},
TabAnimations,
TabsWithNavigationFocus,
};
class MainScreen extends React.Component<*> {
type State = {
scrollY: Animated.Value,
};
class MainScreen extends React.Component<any, State> {
state = {
scrollY: new Animated.Value(0),
};
componentWillMount() {
Asset.fromModule(
require('react-navigation/src/views/assets/back-icon-mask.png')
).downloadAsync();
Asset.fromModule(
require('react-navigation/src/views/assets/back-icon.png')
).downloadAsync();
}
render() {
const { navigation } = this.props;
const scale = this.state.scrollY.interpolate({
inputRange: [-450, 0, 100],
outputRange: [2, 1, 0.8],
extrapolate: 'clamp',
});
const translateY = this.state.scrollY.interpolate({
inputRange: [-450, 0, 100],
outputRange: [-150, 0, 40],
});
const opacity = this.state.scrollY.interpolate({
inputRange: [0, 50],
outputRange: [1, 0],
extrapolate: 'clamp',
});
const underlayOpacity = this.state.scrollY.interpolate({
inputRange: [0, 50],
outputRange: [0, 1],
extrapolate: 'clamp',
});
const backgroundScale = this.state.scrollY.interpolate({
inputRange: [-450, 0],
outputRange: [3, 1],
extrapolate: 'clamp',
});
const backgroundTranslateY = this.state.scrollY.interpolate({
inputRange: [-450, 0],
outputRange: [0, 0],
});
return (
<View style={{ flex: 1 }}>
<ScrollView style={{ flex: 1 }}>
<Banner />
{Object.keys(ExampleRoutes).map((routeName: string) => (
<TouchableOpacity
key={routeName}
onPress={() => {
const { path, params, screen } = ExampleRoutes[routeName];
const { router } = screen;
const action =
path && router.getActionForPathAndParams(path, params);
navigation.navigate(routeName, {}, action);
}}
<Animated.ScrollView
style={{ flex: 1 }}
scrollEventThrottle={1}
onScroll={Animated.event(
[
{
nativeEvent: { contentOffset: { y: this.state.scrollY } },
},
],
{ useNativeDriver: true }
)}
>
<Animated.View
style={[
styles.backgroundUnderlay,
{
transform: [
{ scale: backgroundScale },
{ translateY: backgroundTranslateY },
],
},
]}
/>
<Animated.View
style={{ opacity, transform: [{ scale }, { translateY }] }}
>
<SafeAreaView
style={styles.bannerContainer}
forceInset={{ top: 'always', bottom: 'never' }}
>
<SafeAreaView
style={styles.itemContainer}
forceInset={{ vertical: 'never' }}
>
<View style={styles.item}>
<Text style={styles.title}>
{ExampleInfo[routeName].name}
</Text>
<Text style={styles.description}>
{ExampleInfo[routeName].description}
</Text>
</View>
</SafeAreaView>
</TouchableOpacity>
))}
</ScrollView>
<View style={styles.banner}>
<Image
source={require('./assets/NavLogo.png')}
style={styles.bannerImage}
/>
<Text style={styles.bannerTitle}>
React Navigation Examples
</Text>
</View>
</SafeAreaView>
</Animated.View>
<SafeAreaView forceInset={{ bottom: 'always', horizontal: 'never' }}>
<View style={{ backgroundColor: '#fff' }}>
{Object.keys(ExampleRoutes).map((routeName: string) => (
<TouchableOpacity
key={routeName}
onPress={() => {
let route = ExampleRoutes[routeName];
if (route.screen || route.path || route.params) {
const { path, params, screen } = route;
const { router } = screen;
const action =
path && router.getActionForPathAndParams(path, params);
navigation.navigate(routeName, {}, action);
} else {
navigation.navigate(routeName);
}
}}
>
<SafeAreaView
style={styles.itemContainer}
forceInset={{ veritcal: 'never', bottom: 'never' }}
>
<View style={styles.item}>
<Text style={styles.title}>
{ExampleInfo[routeName].name}
</Text>
<Text style={styles.description}>
{ExampleInfo[routeName].description}
</Text>
</View>
</SafeAreaView>
</TouchableOpacity>
))}
</View>
</SafeAreaView>
</Animated.ScrollView>
<StatusBar barStyle="light-content" />
<View style={styles.statusBarUnderlay} />
<Animated.View
style={[styles.statusBarUnderlay, { opacity: underlayOpacity }]}
/>
</View>
);
}
@@ -194,7 +305,8 @@ const AppNavigator = StackNavigator(
}
);
export default () => <AppNavigator />;
// export default () => <AppNavigator />;
export default SimpleStack;
const styles = StyleSheet.create({
item: {
@@ -230,4 +342,35 @@ const styles = StyleSheet.create({
fontSize: 13,
color: '#999',
},
backgroundUnderlay: {
backgroundColor: '#673ab7',
position: 'absolute',
top: -100,
height: 300,
left: 0,
right: 0,
},
bannerContainer: {
// backgroundColor: '#673ab7',
alignItems: 'center',
},
banner: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
},
bannerImage: {
width: 36,
height: 36,
resizeMode: 'contain',
tintColor: '#fff',
margin: 8,
},
bannerTitle: {
fontSize: 18,
fontWeight: '200',
color: '#fff',
marginVertical: 8,
marginRight: 5,
},
});

View File

@@ -18,7 +18,6 @@ import {
createNavigationContainer,
SafeAreaView,
TabRouter,
addNavigationHelpers,
} from 'react-navigation';
import SampleText from './SampleText';
@@ -66,19 +65,14 @@ const CustomTabBar = ({ navigation }) => {
);
};
const CustomTabView = ({ router, navigation }) => {
const CustomTabView = ({ descriptors, navigation }) => {
const { routes, index } = navigation.state;
const ActiveScreen = router.getComponentForRouteName(routes[index].routeName);
const descriptor = descriptors[routes[index].key];
const ActiveScreen = descriptor.getComponent();
return (
<SafeAreaView forceInset={{ top: 'always' }}>
<CustomTabBar navigation={navigation} />
<ActiveScreen
navigation={addNavigationHelpers({
dispatch: navigation.dispatch,
state: routes[index],
})}
screenProps={{}}
/>
<ActiveScreen navigation={descriptor.navigation} />
</SafeAreaView>
);
};
@@ -105,7 +99,7 @@ const CustomTabRouter = TabRouter(
);
const CustomTabs = createNavigationContainer(
createNavigator(CustomTabRouter)(CustomTabView)
createNavigator(CustomTabView, CustomTabRouter, {})
);
const styles = StyleSheet.create({

View File

@@ -14,7 +14,6 @@ import {
SafeAreaView,
StackRouter,
createNavigationContainer,
addNavigationHelpers,
createNavigator,
} from 'react-navigation';
import SampleText from './SampleText';
@@ -45,11 +44,12 @@ const MySettingsScreen = ({ navigation }) => (
class CustomNavigationView extends Component {
render() {
const { navigation, router } = this.props;
const { navigation, router, descriptors } = this.props;
return (
<Transitioner
configureTransition={this._configureTransition}
descriptors={descriptors}
navigation={navigation}
render={this._render}
/>
@@ -86,16 +86,10 @@ class CustomNavigationView extends Component {
transform: [{ scale: animatedValue }],
};
// The prop `router` is populated when we call `createNavigator`.
const Scene = router.getComponentForRouteName(scene.route.routeName);
const Scene = scene.descriptor.getComponent();
return (
<Animated.View key={index} style={[styles.view, animation]}>
<Scene
navigation={addNavigationHelpers({
...navigation,
state: routes[index],
})}
/>
<Scene navigation={scene.descriptor.navigation} />
</Animated.View>
);
};
@@ -107,7 +101,7 @@ const CustomRouter = StackRouter({
});
const CustomTransitioner = createNavigationContainer(
createNavigator(CustomRouter)(CustomNavigationView)
createNavigator(CustomNavigationView, CustomRouter, {})
);
export default CustomTransitioner;

View File

@@ -4,7 +4,11 @@
import React from 'react';
import { Button, Platform, ScrollView, StatusBar } from 'react-native';
import { StackNavigator, DrawerNavigator, SafeAreaView } from 'react-navigation';
import {
StackNavigator,
DrawerNavigator,
SafeAreaView,
} from 'react-navigation';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import SampleText from './SampleText';
@@ -12,10 +16,7 @@ const MyNavScreen = ({ navigation, banner }) => (
<ScrollView>
<SafeAreaView forceInset={{ top: 'always' }}>
<SampleText>{banner}</SampleText>
<Button
onPress={() => navigation.navigate('DrawerOpen')}
title="Open drawer"
/>
<Button onPress={() => navigation.openDrawer()} title="Open drawer" />
<Button
onPress={() => navigation.navigate('Email')}
title="Open other screen"
@@ -76,9 +77,6 @@ const DrawerExample = DrawerNavigator(
},
},
{
drawerOpenRoute: 'DrawerOpen',
drawerCloseRoute: 'DrawerClose',
drawerToggleRoute: 'DrawerToggle',
initialRouteName: 'Drafts',
contentOptions: {
activeTintColor: '#e91e63',

View File

@@ -8,7 +8,7 @@ import { SafeAreaView, StackNavigator } from 'react-navigation';
import SampleText from './SampleText';
const MyNavScreen = ({ navigation, banner }) => (
<ScrollView contentInsetAdjustmentBehavior="automatic">
<ScrollView>
<SafeAreaView
forceInset={{
top: navigation.state.routeName === 'HeaderTest' ? 'always' : 'never',

View File

@@ -11,10 +11,7 @@ import SampleText from './SampleText';
const MyNavScreen = ({ navigation, banner }) => (
<ScrollView style={styles.container}>
<SampleText>{banner}</SampleText>
<Button
onPress={() => navigation.navigate('DrawerOpen')}
title="Open drawer"
/>
<Button onPress={() => navigation.openDrawer()} title="Open drawer" />
<Button onPress={() => navigation.goBack(null)} title="Go back" />
</ScrollView>
);
@@ -55,9 +52,6 @@ const DrawerExample = DrawerNavigator(
},
},
{
drawerOpenRoute: 'DrawerOpen',
drawerCloseRoute: 'DrawerClose',
drawerToggleRoute: 'DrawerToggle',
initialRouteName: 'Drafts',
contentOptions: {
activeTintColor: '#e91e63',
@@ -69,10 +63,6 @@ const MainDrawerExample = DrawerNavigator({
Drafts: {
screen: DrawerExample,
},
}, {
drawerOpenRoute: 'DrawerOpen',
drawerCloseRoute: 'DrawerClose',
drawerToggleRoute: 'DrawerToggle',
});
const styles = StyleSheet.create({

View File

@@ -9,7 +9,7 @@ import type {
import * as React from 'react';
import { Button, ScrollView, StatusBar } from 'react-native';
import { StackNavigator, SafeAreaView } from 'react-navigation';
import { StackNavigator, SafeAreaView, withNavigation } from 'react-navigation';
import SampleText from './SampleText';
type MyNavScreenProps = {
@@ -17,6 +17,18 @@ type MyNavScreenProps = {
banner: React.Node,
};
class MyBackButton extends React.Component<any, any> {
render() {
return <Button onPress={this._navigateBack} title="Custom Back" />;
}
_navigateBack = () => {
this.props.navigation.goBack(null);
};
}
const MyBackButtonWithNavigation = withNavigation(MyBackButton);
class MyNavScreen extends React.Component<MyNavScreenProps> {
render() {
const { navigation, banner } = this.props;
@@ -94,6 +106,7 @@ type MyPhotosScreenProps = {
class MyPhotosScreen extends React.Component<MyPhotosScreenProps> {
static navigationOptions = {
title: 'Photos',
headerLeft: <MyBackButtonWithNavigation />,
};
_s0: NavigationEventSubscription;
_s1: NavigationEventSubscription;

View File

@@ -163,6 +163,8 @@ const SimpleTabs = TabNavigator(
},
},
{
lazy: true,
removeClippedSubviews: true,
tabBarOptions: {
activeTintColor: Platform.OS === 'ios' ? '#e91e63' : '#fff',
},

View File

@@ -0,0 +1,68 @@
/**
* @flow
*/
import type { NavigationScreenProp } from 'react-navigation';
import * as React from 'react';
import { Button, ScrollView, StatusBar } from 'react-native';
import { StackNavigator, SafeAreaView } from 'react-navigation';
type NavScreenProps = {
navigation: NavigationScreenProp<*>,
};
class HomeScreen extends React.Component<NavScreenProps> {
static navigationOptions = {
title: 'Welcome',
};
render() {
const { navigation } = this.props;
return (
<SafeAreaView style={{ paddingTop: 30 }}>
<Button
onPress={() => navigation.push('Other')}
title="Push another screen"
/>
<Button onPress={() => navigation.pop()} title="Pop" />
<Button onPress={() => navigation.goBack(null)} title="Go back" />
<StatusBar barStyle="default" />
</SafeAreaView>
);
}
}
class OtherScreen extends React.Component<NavScreenProps> {
static navigationOptions = {
title: 'Your title here',
};
render() {
const { navigation } = this.props;
return (
<SafeAreaView style={{ paddingTop: 30 }}>
<Button
onPress={() => navigation.push('Other')}
title="Push another screen"
/>
<Button onPress={() => navigation.pop()} title="Pop" />
<Button onPress={() => navigation.goBack(null)} title="Go back" />
<StatusBar barStyle="default" />
</SafeAreaView>
);
}
}
const StackWithHeaderPreset = StackNavigator(
{
Home: HomeScreen,
Other: OtherScreen,
},
{
headerTransitionPreset: 'uikit',
}
);
export default StackWithHeaderPreset;

View File

@@ -0,0 +1,234 @@
/**
* @flow
*/
import type {
NavigationScreenProp,
NavigationEventSubscription,
} from 'react-navigation';
import { isIphoneX } from 'react-native-iphone-x-helper';
import * as React from 'react';
import { BlurView, Constants } from 'expo';
import {
Button,
Dimensions,
Platform,
ScrollView,
StatusBar,
View,
} from 'react-native';
import { Header, StackNavigator } from 'react-navigation';
import SampleText from './SampleText';
type MyNavScreenProps = {
navigation: NavigationScreenProp<*>,
banner: React.Node,
};
class MyNavScreen extends React.Component<MyNavScreenProps> {
render() {
const { navigation, banner } = this.props;
return (
<ScrollView style={{ flex: 1 }} {...this.getHeaderInset()}>
<SampleText>{banner}</SampleText>
<Button
onPress={() => navigation.push('Profile', { name: 'Jane' })}
title="Push a profile screen"
/>
<Button
onPress={() => navigation.navigate('Photos', { name: 'Jane' })}
title="Navigate to a photos screen"
/>
<Button
onPress={() => navigation.replace('Profile', { name: 'Lucy' })}
title="Replace with profile"
/>
<Button onPress={() => navigation.popToTop()} title="Pop to top" />
<Button onPress={() => navigation.pop()} title="Pop" />
<Button onPress={() => navigation.goBack(null)} title="Go back" />
<StatusBar barStyle="default" />
</ScrollView>
);
}
// Inset to compensate for navigation bar being transparent.
// And improved abstraction for this will be built in to react-navigation
// at some point.
getHeaderInset() {
const NOTCH_HEIGHT = isIphoneX() ? 25 : 0;
// $FlowIgnore: we will remove the HEIGHT static soon enough
const BASE_HEADER_HEIGHT = Header.HEIGHT;
const HEADER_HEIGHT =
Platform.OS === 'ios'
? BASE_HEADER_HEIGHT + NOTCH_HEIGHT
: BASE_HEADER_HEIGHT + Constants.statusBarHeight;
return Platform.select({
ios: {
contentInset: { top: HEADER_HEIGHT },
contentOffset: { y: -HEADER_HEIGHT },
},
android: {
contentContainerStyle: {
paddingTop: HEADER_HEIGHT,
},
},
});
}
}
type MyHomeScreenProps = {
navigation: NavigationScreenProp<*>,
};
class MyHomeScreen extends React.Component<MyHomeScreenProps> {
static navigationOptions = {
title: 'Welcome',
};
_s0: NavigationEventSubscription;
_s1: NavigationEventSubscription;
_s2: NavigationEventSubscription;
_s3: NavigationEventSubscription;
componentDidMount() {
this._s0 = this.props.navigation.addListener('willFocus', this._onWF);
this._s1 = this.props.navigation.addListener('didFocus', this._onDF);
this._s2 = this.props.navigation.addListener('willBlur', this._onWB);
this._s3 = this.props.navigation.addListener('didBlur', this._onDB);
}
componentWillUnmount() {
this._s0.remove();
this._s1.remove();
this._s2.remove();
this._s3.remove();
}
_onWF = a => {
console.log('_willFocus HomeScreen', a);
};
_onDF = a => {
console.log('_didFocus HomeScreen', a);
};
_onWB = a => {
console.log('_willBlur HomeScreen', a);
};
_onDB = a => {
console.log('_didBlur HomeScreen', a);
};
render() {
const { navigation } = this.props;
return <MyNavScreen banner="Home Screen" navigation={navigation} />;
}
}
type MyPhotosScreenProps = {
navigation: NavigationScreenProp<*>,
};
class MyPhotosScreen extends React.Component<MyPhotosScreenProps> {
static navigationOptions = {
title: 'Photos',
};
_s0: NavigationEventSubscription;
_s1: NavigationEventSubscription;
_s2: NavigationEventSubscription;
_s3: NavigationEventSubscription;
componentDidMount() {
this._s0 = this.props.navigation.addListener('willFocus', this._onWF);
this._s1 = this.props.navigation.addListener('didFocus', this._onDF);
this._s2 = this.props.navigation.addListener('willBlur', this._onWB);
this._s3 = this.props.navigation.addListener('didBlur', this._onDB);
}
componentWillUnmount() {
this._s0.remove();
this._s1.remove();
this._s2.remove();
this._s3.remove();
}
_onWF = a => {
console.log('_willFocus PhotosScreen', a);
};
_onDF = a => {
console.log('_didFocus PhotosScreen', a);
};
_onWB = a => {
console.log('_willBlur PhotosScreen', a);
};
_onDB = a => {
console.log('_didBlur PhotosScreen', a);
};
render() {
const { navigation } = this.props;
return (
<MyNavScreen
banner={`${navigation.state.params.name}'s Photos`}
navigation={navigation}
/>
);
}
}
const MyProfileScreen = ({ navigation }) => (
<MyNavScreen
banner={`${navigation.state.params.mode === 'edit' ? 'Now Editing ' : ''}${
navigation.state.params.name
}'s Profile`}
navigation={navigation}
/>
);
MyProfileScreen.navigationOptions = props => {
const { navigation } = props;
const { state, setParams } = navigation;
const { params } = state;
return {
headerBackImage: params.headerBackImage,
headerTitle: `${params.name}'s Profile!`,
// Render a button on the right side of the header.
// When pressed switches the screen to edit mode.
headerRight: (
<Button
title={params.mode === 'edit' ? 'Done' : 'Edit'}
onPress={() =>
setParams({ mode: params.mode === 'edit' ? '' : 'edit' })
}
/>
),
};
};
const StackWithTranslucentHeader = StackNavigator(
{
Home: {
screen: MyHomeScreen,
},
Profile: {
path: 'people/:name',
screen: MyProfileScreen,
},
Photos: {
path: 'photos/:name',
screen: MyPhotosScreen,
},
},
{
headerTransitionPreset: 'uikit',
navigationOptions: {
headerTransparent: true,
headerBackground: Platform.select({
ios: <BlurView style={{ flex: 1 }} intensity={98} />,
android: (
<View style={{ flex: 1, backgroundColor: 'rgba(255,255,255,0.7)' }} />
),
}),
},
}
);
export default StackWithTranslucentHeader;

View File

@@ -0,0 +1,101 @@
import React from 'react';
import { Button, StatusBar, Text, View } from 'react-native';
import { StackNavigator } from 'react-navigation';
class HomeScreen extends React.Component<any, any> {
render() {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Home</Text>
<Button
title="Navigate to 'Profile' with key 'A'"
onPress={() =>
this.props.navigation.navigate({
routeName: 'Profile',
key: 'A',
params: { homeKey: this.props.navigation.state.key },
})
}
/>
<Button
title="Go back to other examples"
onPress={() => this.props.navigation.goBack(null)}
/>
<StatusBar barStyle="default" />
</View>
);
}
}
class ProfileScreen extends React.Component<any, any> {
render() {
const { homeKey } = this.props.navigation.state.params;
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Profile</Text>
<Button
title="Navigate to 'Settings' with key 'B'"
onPress={() =>
this.props.navigation.navigate({
routeName: 'Settings',
key: 'B',
params: { homeKey },
})
}
/>
<Button
title={`Navigate back to 'Home' with key ${homeKey}`}
onPress={() =>
this.props.navigation.navigate({ routeName: 'Home', key: homeKey })
}
/>
</View>
);
}
}
class SettingsScreen extends React.Component<any, any> {
render() {
const { homeKey } = this.props.navigation.state.params;
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Settings</Text>
<Button
title={`Navigate back to 'Home' with key ${homeKey}`}
onPress={() =>
this.props.navigation.navigate({ routeName: 'Home', key: homeKey })
}
/>
<Button
title="Navigate back to 'Profile' with key 'A'"
onPress={() =>
this.props.navigation.navigate({
routeName: 'Profile',
key: 'A',
})
}
/>
</View>
);
}
}
const Stack = StackNavigator(
{
Home: {
screen: HomeScreen,
},
Profile: {
screen: ProfileScreen,
},
Settings: {
screen: SettingsScreen,
},
},
{
headerMode: 'none',
}
);
export default Stack;

View File

@@ -0,0 +1,101 @@
/**
* @flow
*/
import React from 'react';
import { Button, SafeAreaView, Text } from 'react-native';
import { TabNavigator, withNavigationFocus } from 'react-navigation';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import SampleText from './SampleText';
class Child extends React.Component<any, any> {
render() {
return (
<Text style={{ color: this.props.isFocused ? 'green' : 'maroon' }}>
{this.props.isFocused
? 'I know that my parent is focused!'
: 'My parent is not focused! :O'}
</Text>
);
}
}
const ChildWithNavigationFocus = withNavigationFocus(Child);
const createTabScreen = (name, icon, focusedIcon, tintColor = '#673ab7') => {
class TabScreen extends React.Component<any, any> {
static navigationOptions = {
tabBarLabel: name,
tabBarIcon: ({ tintColor, focused }) => (
<MaterialCommunityIcons
name={focused ? focusedIcon : icon}
size={26}
style={{ color: focused ? tintColor : '#ccc' }}
/>
),
};
state = { showChild: false };
render() {
const { isFocused } = this.props;
return (
<SafeAreaView
forceInset={{ horizontal: 'always', top: 'always' }}
style={{
flex: 1,
alignItems: 'center',
justifyContent: 'center',
}}
>
<Text style={{ fontWeight: '700', fontSize: 16, marginBottom: 5 }}>
{'Tab ' + name.toLowerCase()}
</Text>
<Text style={{ marginBottom: 20 }}>
{'props.isFocused: ' + (isFocused ? ' true' : 'false')}
</Text>
{this.state.showChild ? (
<ChildWithNavigationFocus />
) : (
<Button
title="Press me"
onPress={() => this.setState({ showChild: true })}
/>
)}
<Button
onPress={() => this.props.navigation.goBack(null)}
title="Back to other examples"
/>
</SafeAreaView>
);
}
}
return withNavigationFocus(TabScreen);
};
const TabsWithNavigationFocus = TabNavigator(
{
One: {
screen: createTabScreen('One', 'numeric-1-box-outline', 'numeric-1-box'),
},
Two: {
screen: createTabScreen('Two', 'numeric-2-box-outline', 'numeric-2-box'),
},
Three: {
screen: createTabScreen(
'Three',
'numeric-3-box-outline',
'numeric-3-box'
),
},
},
{
tabBarPosition: 'bottom',
animationEnabled: true,
swipeEnabled: true,
}
);
export default TabsWithNavigationFocus;

View File

@@ -14,10 +14,12 @@
"expo": "^25.0.0",
"react": "16.2.0",
"react-native": "^0.52.0",
"react-native-iphone-x-helper": "^1.0.2",
"react-navigation": "link:../.."
},
"devDependencies": {
"babel-jest": "^21.0.0",
"babel-plugin-transform-remove-console": "^6.9.0",
"flow-bin": "^0.61.0",
"jest": "^21.0.1",
"jest-expo": "^25.1.0",

File diff suppressed because it is too large Load Diff

View File

@@ -2,4 +2,4 @@
## Usage
Please see the [Contributors Guide](https://reactnavigation.org/docs/guides/contributors#Run-the-Example-App) for instructions on running these example apps.
Please see the [Contributors Guide](https://reactnavigation.org/docs/contributing.html#run-the-example-app) for instructions on running these example apps.

View File

@@ -48,6 +48,15 @@ declare module 'react-navigation' {
// react-native/Libraries/Animated/src/nodes/AnimatedValue.js
declare type AnimatedValue = Object;
declare type HeaderForceInset = {
horizontal?: string,
vertical?: string,
left?: string,
right?: string,
top?: string,
bottom?: string,
};
/**
* Next, all the type declarations
*/
@@ -71,25 +80,11 @@ declare module 'react-navigation' {
key?: string,
|};
declare type DeprecatedNavigationNavigateAction = {|
type: 'Navigate',
routeName: string,
params?: NavigationParams,
// The action to run inside the sub-router
action?: NavigationNavigateAction | DeprecatedNavigationNavigateAction,
|};
declare export type NavigationBackAction = {|
type: 'Navigation/BACK',
key?: ?string,
|};
declare type DeprecatedNavigationBackAction = {|
type: 'Back',
key?: ?string,
|};
declare export type NavigationSetParamsAction = {|
type: 'Navigation/SET_PARAMS',
@@ -100,26 +95,11 @@ declare module 'react-navigation' {
params: NavigationParams,
|};
declare type DeprecatedNavigationSetParamsAction = {|
type: 'SetParams',
// The key of the route where the params should be set
key: string,
// The new params to merge into the existing route params
params: NavigationParams,
|};
declare export type NavigationInitAction = {|
type: 'Navigation/INIT',
params?: NavigationParams,
|};
declare type DeprecatedNavigationInitAction = {|
type: 'Init',
params?: NavigationParams,
|};
declare export type NavigationResetAction = {|
type: 'Navigation/RESET',
index: number,
@@ -127,25 +107,11 @@ declare module 'react-navigation' {
actions: Array<NavigationNavigateAction>,
|};
declare type DeprecatedNavigationResetAction = {|
type: 'Reset',
index: number,
key?: ?string,
actions: Array<
NavigationNavigateAction | DeprecatedNavigationNavigateAction
>,
|};
declare export type NavigationUriAction = {|
type: 'Navigation/URI',
uri: string,
|};
declare type DeprecatedNavigationUriAction = {|
type: 'Uri',
uri: string,
|};
declare export type NavigationReplaceAction = {|
+type: 'Navigation/REPLACE',
+key: string,
@@ -181,17 +147,6 @@ declare module 'react-navigation' {
| NavigationSetParamsAction
| NavigationResetAction;
declare type DeprecatedNavigationAction =
| DeprecatedNavigationInitAction
| DeprecatedNavigationNavigateAction
| DeprecatedNavigationBackAction
| DeprecatedNavigationSetParamsAction
| DeprecatedNavigationResetAction;
declare export type PossiblyDeprecatedNavigationAction =
| NavigationAction
| DeprecatedNavigationAction;
/**
* NavigationState is a tree of routes for a single navigator, where each
* child route may either be a NavigationScreenRoute or a
@@ -314,7 +269,8 @@ declare module 'react-navigation' {
declare export type NavigationComponent =
| NavigationScreenComponent<NavigationRoute, *, *>
| NavigationContainer<NavigationStateRoute, *, *>;
| NavigationContainer<*, *, *>
| any;
declare export type NavigationScreenComponent<
Route: NavigationRoute,
@@ -332,12 +288,15 @@ declare module 'react-navigation' {
navigationOptions?: ?NavigationScreenConfig<Options>,
};
declare export type NavigationRouteConfig = {
navigationOptions?: NavigationScreenConfig<*>,
path?: string,
} & NavigationScreenRouteConfig;
declare export type NavigationRouteConfig =
| NavigationComponent
| ({
navigationOptions?: NavigationScreenConfig<*>,
path?: string,
} & NavigationScreenRouteConfig);
declare export type NavigationScreenRouteConfig =
| NavigationComponent
| {
screen: NavigationComponent,
}
@@ -378,6 +337,7 @@ declare module 'react-navigation' {
declare export type NavigationStackScreenOptions = NavigationScreenOptions & {
header?: ?(React$Node | (HeaderProps => React$Node)),
headerTransparent?: boolean,
headerTitle?: string | React$Node | React$ElementType,
headerTitleStyle?: AnimatedTextStyleProp,
headerTitleAllowFontScaling?: boolean,
@@ -390,6 +350,8 @@ declare module 'react-navigation' {
headerPressColorAndroid?: string,
headerRight?: React$Node,
headerStyle?: ViewStyleProp,
headerForceInset?: HeaderForceInset,
headerBackground?: React$Node | React$ElementType,
gesturesEnabled?: boolean,
gestureResponseDistance?: { vertical?: number, horizontal?: number },
gestureDirection?: 'default' | 'inverted',
@@ -400,11 +362,13 @@ declare module 'react-navigation' {
initialRouteParams?: NavigationParams,
paths?: NavigationPathsConfig,
navigationOptions?: NavigationScreenConfig<*>,
initialRouteKey?: string,
|};
declare export type NavigationStackViewConfig = {|
mode?: 'card' | 'modal',
headerMode?: HeaderMode,
headerTransitionPreset?: 'fade-in-place' | 'uikit',
cardStyle?: ViewStyleProp,
transitionConfig?: () => TransitionConfig,
onTransitionStart?: () => void,
@@ -476,7 +440,7 @@ declare module 'react-navigation' {
*/
declare export type NavigationDispatch = (
action: PossiblyDeprecatedNavigationAction
action: NavigationAction
) => boolean;
declare export type NavigationProp<S> = {
@@ -495,7 +459,7 @@ declare module 'react-navigation' {
type: EventType,
action: NavigationAction,
state: NavigationState,
lastState: NavigationState,
lastState: ?NavigationState,
};
declare export type NavigationEventCallback = (
@@ -540,29 +504,6 @@ declare module 'react-navigation' {
navigationOptions?: O,
}>;
//declare export type NavigationNavigatorProps<O: {}, S: {}> =
// | {}
// | { navigation: NavigationScreenProp<S> }
// | { screenProps: {} }
// | { navigationOptions: O }
// | {
// navigation: NavigationScreenProp<S>,
// screenProps: {},
// }
// | {
// navigation: NavigationScreenProp<S>,
// navigationOptions: O,
// }
// | {
// screenProps: {},
// navigationOptions: O,
// }
// | {
// navigation: NavigationScreenProp<S>,
// screenProps: {},
// navigationOptions: O,
// };
/**
* Navigation container
*/
@@ -737,10 +678,6 @@ declare module 'react-navigation' {
) => NavigationState,
};
declare export function addNavigationHelpers<S: {}>(
navigation: NavigationProp<S>
): NavigationScreenProp<S>;
declare export var NavigationActions: {
BACK: 'Navigation/BACK',
INIT: 'Navigation/INIT',
@@ -749,11 +686,11 @@ declare module 'react-navigation' {
SET_PARAMS: 'Navigation/SET_PARAMS',
URI: 'Navigation/URI',
back: {
(payload: { key?: ?string }): NavigationBackAction,
(payload?: { key?: ?string }): NavigationBackAction,
toString: () => string,
},
init: {
(payload: { params?: NavigationParams }): NavigationInitAction,
(payload?: { params?: NavigationParams }): NavigationInitAction,
toString: () => string,
},
navigate: {
@@ -783,31 +720,29 @@ declare module 'react-navigation' {
(payload: { uri: string }): NavigationUriAction,
toString: () => string,
},
mapDeprecatedActionAndWarn: (
action: PossiblyDeprecatedNavigationAction
) => NavigationAction,
};
declare type _RouterProp<S: NavigationState, O: {}> = {
router: NavigationRouter<S, O>,
};
declare type _NavigatorCreator<
NavigationViewProps: {},
S: NavigationState,
O: {}
> = (
NavigationView: React$ComponentType<_RouterProp<S, O> & NavigationViewProps>
) => NavigationNavigator<S, O, NavigationViewProps>;
declare export function createNavigator<
S: NavigationState,
O: {},
NavigatorConfig: {},
NavigationViewProps: NavigationNavigatorProps<O, S>
>(
declare type NavigationDescriptor = {
key: string,
state: NavigationLeafRoute | NavigationStateRoute,
navigation: NavigationScreenProp<*>,
getComponent: () => React$ComponentType<{}>,
};
declare type NavigationView<O, S> = React$ComponentType<{
descriptors: { [key: string]: NavigationDescriptor },
navigation: NavigationScreenProp<S>,
}>;
declare export function createNavigator<O: *, S: *, NavigatorConfig: *>(
view: NavigationView<O, S>,
router: NavigationRouter<S, O>,
routeConfigs?: NavigationRouteConfigMap,
navigatorConfig?: NavigatorConfig
): _NavigatorCreator<NavigationViewProps, S, O>;
): any;
declare export function StackNavigator(
routeConfigMap: NavigationRouteConfigMap,
@@ -829,6 +764,8 @@ declare module 'react-navigation' {
declare type _TabNavigatorConfig = {|
...NavigationTabRouterConfig,
..._TabViewConfig,
lazy?: boolean,
removeClippedSubviews?: boolean,
containerOptions?: void,
|};
declare export function TabNavigator(
@@ -840,9 +777,6 @@ declare module 'react-navigation' {
drawerLockMode?: 'unlocked' | 'locked-closed' | 'locked-open',
drawerWidth?: number | (() => number),
drawerPosition?: 'left' | 'right',
drawerOpenRoute?: string,
drawerCloseRoute?: string,
drawerToggleRoute?: string,
contentComponent?: React$ElementType,
contentOptions?: {},
style?: ViewStyleProp,
@@ -975,9 +909,6 @@ declare module 'react-navigation' {
drawerLockMode?: 'unlocked' | 'locked-closed' | 'locked-open',
drawerWidth: number | (() => number),
drawerPosition: 'left' | 'right',
drawerOpenRoute: string,
drawerCloseRoute: string,
drawerToggleRoute: string,
contentComponent: React$ElementType,
contentOptions?: {},
style?: ViewStyleProp,
@@ -1014,6 +945,8 @@ declare module 'react-navigation' {
itemsContainerStyle?: ViewStyleProp,
itemStyle?: ViewStyleProp,
labelStyle?: TextStyleProp,
activeLabelStyle?: TextStyleProp,
inactiveLabelStyle?: TextStyleProp,
iconContainerStyle?: ViewStyleProp,
drawerPosition: 'left' | 'right',
};
@@ -1094,9 +1027,12 @@ declare module 'react-navigation' {
declare export var TabBarBottom: React$ComponentType<_TabBarBottomProps>;
declare type _NavigationInjectedProps = {
navigation: NavigationScreenProp<NavigationState>,
navigation: NavigationScreenProp<NavigationStateRoute>,
};
declare export function withNavigation<T: {}>(
Component: React$ComponentType<T & _NavigationInjectedProps>
): React$ComponentType<T>;
declare export function withNavigationFocus<T: {}>(
Component: React$ComponentType<T & _NavigationInjectedProps>
): React$ComponentType<T>;
}

View File

@@ -1,6 +1,6 @@
{
"name": "react-navigation",
"version": "1.0.0",
"version": "1.2.1",
"description": "Routing and navigation for your React Native apps",
"main": "src/react-navigation.js",
"repository": {
@@ -33,6 +33,7 @@
"path-to-regexp": "^1.7.0",
"prop-types": "^15.5.10",
"react-native-drawer-layout-polyfill": "^1.3.2",
"react-native-safe-area-view": "^0.7.0",
"react-native-tab-view": "^0.0.74"
},
"devDependencies": {
@@ -71,6 +72,10 @@
"modulePathIgnorePatterns": ["examples"]
},
"lint-staged": {
"*.js": ["eslint --fix", "git add"]
"*.js": [
"eslint --fix",
"prettier --write flow/react-navigation.js",
"git add"
]
}
}

View File

@@ -9,6 +9,9 @@ const REPLACE = 'Navigation/REPLACE';
const SET_PARAMS = 'Navigation/SET_PARAMS';
const URI = 'Navigation/URI';
const COMPLETE_TRANSITION = 'Navigation/COMPLETE_TRANSITION';
const OPEN_DRAWER = 'Navigation/OPEN_DRAWER';
const CLOSE_DRAWER = 'Navigation/CLOSE_DRAWER';
const TOGGLE_DRAWER = 'Navigation/TOGGLE_DRAWER';
const createAction = (type, fn) => {
fn.toString = () => type;
@@ -57,6 +60,7 @@ const pop = createAction(POP, payload => ({
const popToTop = createAction(POP_TO_TOP, payload => ({
type: POP_TO_TOP,
immediate: payload && payload.immediate,
key: payload && payload.key,
}));
const push = createAction(PUSH, payload => {
@@ -106,58 +110,15 @@ const completeTransition = createAction(COMPLETE_TRANSITION, payload => ({
key: payload && payload.key,
}));
const mapDeprecatedNavigateAction = action => {
if (action.type === 'Navigate') {
const payload = {
routeName: action.routeName,
params: action.params,
};
if (action.action) {
payload.action = mapDeprecatedNavigateAction(action.action);
}
return navigate(payload);
}
return action;
};
const mapDeprecatedAction = action => {
if (action.type === 'Back') {
return back(action);
} else if (action.type === 'Init') {
return init(action);
} else if (action.type === 'Navigate') {
return mapDeprecatedNavigateAction(action);
} else if (action.type === 'Reset') {
return reset({
index: action.index,
key: action.key,
actions: action.actions.map(mapDeprecatedNavigateAction),
});
} else if (action.type === 'SetParams') {
return setParams(action);
}
return action;
};
const mapDeprecatedActionAndWarn = action => {
const newAction = mapDeprecatedAction(action);
if (newAction !== action) {
const oldType = action.type;
const newType = newAction.type;
console.warn(
[
`The action type '${oldType}' has been renamed to '${newType}'.`,
`'${oldType}' will continue to work while in beta but will be removed`,
'in the first major release. Moving forward, you should use the',
'action constants and action creators exported by this library in',
"the 'actions' object.",
'See https://github.com/react-community/react-navigation/pull/120 for',
'more details.',
].join(' ')
);
}
return newAction;
};
const openDrawer = createAction(OPEN_DRAWER, payload => ({
type: OPEN_DRAWER,
}));
const closeDrawer = createAction(CLOSE_DRAWER, payload => ({
type: CLOSE_DRAWER,
}));
const toggleDrawer = createAction(TOGGLE_DRAWER, payload => ({
type: TOGGLE_DRAWER,
}));
export default {
// Action constants
@@ -172,6 +133,9 @@ export default {
SET_PARAMS,
URI,
COMPLETE_TRANSITION,
OPEN_DRAWER,
CLOSE_DRAWER,
TOGGLE_DRAWER,
// Action creators
back,
@@ -185,7 +149,7 @@ export default {
setParams,
uri,
completeTransition,
// TODO: Remove once old actions are deprecated
mapDeprecatedActionAndWarn,
openDrawer,
closeDrawer,
toggleDrawer,
};

View File

@@ -1,9 +1,9 @@
import {
BackAndroid as DeprecatedBackAndroid,
BackHandler as ModernBackHandler,
Linking,
MaskedViewIOS,
} from 'react-native';
const BackHandler = ModernBackHandler || DeprecatedBackAndroid;
export { BackHandler, Linking };
export { BackHandler, MaskedViewIOS };

View File

@@ -1,9 +1,6 @@
export const Linking = {
addEventListener: () => {},
removeEventListener: () => {},
getInitialURL: () => Promise.reject('Unsupported platform'),
};
import React from 'react';
import { BackHandler, View } from 'react-native';
export const BackHandler = {
addEventListener: () => {},
};
const MaskedViewIOS = () => <View>{this.props.children}</View>;
export { BackHandler, MaskedViewIOS };

View File

@@ -4,7 +4,7 @@ import 'react-native';
import renderer from 'react-test-renderer';
import NavigationActions from '../NavigationActions';
import StackNavigator from '../navigators/StackNavigator';
import StackNavigator from '../navigators/createStackNavigator';
const FooScreen = () => <div />;
const BarScreen = () => <div />;

View File

@@ -8,7 +8,6 @@ describe('StateUtils', () => {
const state = {
index: 0,
routes: [{ key: 'a', routeName }],
isTransitioning: false,
};
expect(NavigationStateUtils.get(state, 'a')).toEqual({
key: 'a',
@@ -21,7 +20,6 @@ describe('StateUtils', () => {
const state = {
index: 1,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
expect(NavigationStateUtils.indexOf(state, 'a')).toBe(0);
expect(NavigationStateUtils.indexOf(state, 'b')).toBe(1);
@@ -32,7 +30,6 @@ describe('StateUtils', () => {
const state = {
index: 0,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
expect(NavigationStateUtils.has(state, 'b')).toBe(true);
expect(NavigationStateUtils.has(state, 'c')).toBe(false);
@@ -43,11 +40,9 @@ describe('StateUtils', () => {
const state = {
index: 0,
routes: [{ key: 'a', routeName }],
isTransitioning: false,
};
const newState = {
index: 1,
isTransitioning: false,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
};
expect(NavigationStateUtils.push(state, { key: 'b', routeName })).toEqual(
@@ -59,7 +54,6 @@ describe('StateUtils', () => {
const state = {
index: 0,
routes: [{ key: 'a', routeName }],
isTransitioning: false,
};
expect(() =>
NavigationStateUtils.push(state, { key: 'a', routeName })
@@ -71,12 +65,10 @@ describe('StateUtils', () => {
const state = {
index: 1,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
const newState = {
index: 0,
routes: [{ key: 'a', routeName }],
isTransitioning: false,
};
expect(NavigationStateUtils.pop(state)).toEqual(newState);
});
@@ -85,7 +77,6 @@ describe('StateUtils', () => {
const state = {
index: 0,
routes: [{ key: 'a', routeName }],
isTransitioning: false,
};
expect(NavigationStateUtils.pop(state)).toBe(state);
});
@@ -95,12 +86,10 @@ describe('StateUtils', () => {
const state = {
index: 0,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
const newState = {
index: 1,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
expect(NavigationStateUtils.jumpToIndex(state, 0)).toBe(state);
expect(NavigationStateUtils.jumpToIndex(state, 1)).toEqual(newState);
@@ -110,7 +99,6 @@ describe('StateUtils', () => {
const state = {
index: 0,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
expect(() => NavigationStateUtils.jumpToIndex(state, 2)).toThrow();
});
@@ -119,12 +107,10 @@ describe('StateUtils', () => {
const state = {
index: 0,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
const newState = {
index: 1,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
expect(NavigationStateUtils.jumpTo(state, 'a')).toBe(state);
expect(NavigationStateUtils.jumpTo(state, 'b')).toEqual(newState);
@@ -134,7 +120,6 @@ describe('StateUtils', () => {
const state = {
index: 0,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
expect(() => NavigationStateUtils.jumpTo(state, 'c')).toThrow();
});
@@ -143,12 +128,10 @@ describe('StateUtils', () => {
const state = {
index: 1,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
const newState = {
index: 0,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
expect(NavigationStateUtils.back(state)).toEqual(newState);
expect(NavigationStateUtils.back(newState)).toBe(newState);
@@ -158,12 +141,10 @@ describe('StateUtils', () => {
const state = {
index: 0,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
const newState = {
index: 1,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
expect(NavigationStateUtils.forward(state)).toEqual(newState);
expect(NavigationStateUtils.forward(newState)).toBe(newState);
@@ -174,12 +155,10 @@ describe('StateUtils', () => {
const state = {
index: 0,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
const newState = {
index: 1,
routes: [{ key: 'a', routeName }, { key: 'c', routeName }],
isTransitioning: false,
};
expect(
NavigationStateUtils.replaceAt(state, 'b', { key: 'c', routeName })
@@ -190,12 +169,10 @@ describe('StateUtils', () => {
const state = {
index: 0,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
const newState = {
index: 1,
routes: [{ key: 'a', routeName }, { key: 'c', routeName }],
isTransitioning: false,
};
expect(
NavigationStateUtils.replaceAtIndex(state, 1, { key: 'c', routeName })
@@ -206,7 +183,6 @@ describe('StateUtils', () => {
const state = {
index: 0,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
expect(
NavigationStateUtils.replaceAtIndex(state, 1, state.routes[1])
@@ -218,12 +194,10 @@ describe('StateUtils', () => {
const state = {
index: 0,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
const newState = {
index: 1,
routes: [{ key: 'x', routeName }, { key: 'y', routeName }],
isTransitioning: false,
};
expect(
NavigationStateUtils.reset(state, [
@@ -241,12 +215,10 @@ describe('StateUtils', () => {
const state = {
index: 0,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
const newState = {
index: 0,
routes: [{ key: 'x', routeName }, { key: 'y', routeName }],
isTransitioning: false,
};
expect(
NavigationStateUtils.reset(

View File

@@ -76,4 +76,43 @@ describe('addNavigationHelpers', () => {
});
expect(mockedDispatch.mock.calls.length).toBe(1);
});
it('handles GetParams action', () => {
const mockedDispatch = jest
.fn(() => false)
.mockImplementationOnce(() => true);
expect(
addNavigationHelpers({
state: { key: 'B', routeName: 'Settings', params: { name: 'Peter' } },
dispatch: mockedDispatch,
addListener: dummyEventSubscriber,
}).getParam('name', 'Brent')
).toEqual('Peter');
});
it('handles GetParams action with default param', () => {
const mockedDispatch = jest
.fn(() => false)
.mockImplementationOnce(() => true);
expect(
addNavigationHelpers({
state: { key: 'B', routeName: 'Settings' },
dispatch: mockedDispatch,
addListener: dummyEventSubscriber,
}).getParam('name', 'Brent')
).toEqual('Brent');
});
it('handles GetParams action with param value as null', () => {
const mockedDispatch = jest
.fn(() => false)
.mockImplementationOnce(() => true);
expect(
addNavigationHelpers({
state: { key: 'B', routeName: 'Settings', params: { name: null } },
dispatch: mockedDispatch,
addListener: dummyEventSubscriber,
}).getParam('name')
).toEqual(null);
});
});

View File

@@ -11,10 +11,8 @@ test('child action events only flow when focused', () => {
};
const subscriptionRemove = () => {};
parentSubscriber.mockReturnValueOnce({ remove: subscriptionRemove });
const childEventSubscriber = getChildEventSubscriber(
parentSubscriber,
'key1'
);
const childEventSubscriber = getChildEventSubscriber(parentSubscriber, 'key1')
.addListener;
const testState = {
key: 'foo',
routeName: 'FooRoute',
@@ -66,11 +64,9 @@ test('grandchildren subscription', () => {
const parentSubscriber = getChildEventSubscriber(
grandParentSubscriber,
'parent'
);
const childEventSubscriber = getChildEventSubscriber(
parentSubscriber,
'key1'
);
).addListener;
const childEventSubscriber = getChildEventSubscriber(parentSubscriber, 'key1')
.addListener;
const parentBlurState = {
key: 'foo',
routeName: 'FooRoute',
@@ -135,11 +131,9 @@ test('grandchildren transitions', () => {
const parentSubscriber = getChildEventSubscriber(
grandParentSubscriber,
'parent'
);
const childEventSubscriber = getChildEventSubscriber(
parentSubscriber,
'key1'
);
).addListener;
const childEventSubscriber = getChildEventSubscriber(parentSubscriber, 'key1')
.addListener;
const makeFakeState = (childIndex, childIsTransitioning) => ({
index: 1,
isTransitioning: false,
@@ -230,11 +224,9 @@ test('grandchildren pass through transitions', () => {
const parentSubscriber = getChildEventSubscriber(
grandParentSubscriber,
'parent'
);
const childEventSubscriber = getChildEventSubscriber(
parentSubscriber,
'key1'
);
).addListener;
const childEventSubscriber = getChildEventSubscriber(parentSubscriber, 'key1')
.addListener;
const makeFakeState = (childIndex, childIsTransitioning) => ({
index: childIndex,
isTransitioning: childIsTransitioning,
@@ -322,10 +314,8 @@ test('child focus with transition', () => {
};
const subscriptionRemove = () => {};
parentSubscriber.mockReturnValueOnce({ remove: subscriptionRemove });
const childEventSubscriber = getChildEventSubscriber(
parentSubscriber,
'key1'
);
const childEventSubscriber = getChildEventSubscriber(parentSubscriber, 'key1')
.addListener;
const randomAction = { type: 'FooAction' };
const testState = {
key: 'foo',
@@ -417,10 +407,8 @@ test('child focus with immediate transition', () => {
};
const subscriptionRemove = () => {};
parentSubscriber.mockReturnValueOnce({ remove: subscriptionRemove });
const childEventSubscriber = getChildEventSubscriber(
parentSubscriber,
'key1'
);
const childEventSubscriber = getChildEventSubscriber(parentSubscriber, 'key1')
.addListener;
const randomAction = { type: 'FooAction' };
const testState = {
key: 'foo',

View File

@@ -61,6 +61,16 @@ export default function(navigation) {
return navigation.dispatch(NavigationActions.setParams({ params, key }));
},
getParam: (paramName, defaultValue) => {
const params = navigation.state.params;
if (params && paramName in params) {
return params[paramName];
}
return defaultValue;
},
push: (routeName, params, action) =>
navigation.dispatch(
NavigationActions.push({ routeName, params, action })
@@ -75,5 +85,9 @@ export default function(navigation) {
key: navigation.state.key,
})
),
openDrawer: () => navigation.dispatch(NavigationActions.openDrawer()),
closeDrawer: () => navigation.dispatch(NavigationActions.closeDrawer()),
toggleDrawer: () => navigation.dispatch(NavigationActions.toggleDrawer()),
};
}

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { BackHandler, Linking } from './PlatformHelpers';
import { Linking } from 'react-native';
import { BackHandler } from './PlatformHelpers';
import NavigationActions from './NavigationActions';
import addNavigationHelpers from './addNavigationHelpers';
import invariant from './utils/invariant';
@@ -25,6 +26,20 @@ export default function createNavigationContainer(Component) {
this._validateProps(props);
this._initialAction = NavigationActions.init();
if (this._isStateful()) {
this.subs = BackHandler.addEventListener('hardwareBackPress', () => {
if (!this._isMounted) {
this.subs && this.subs.remove();
} else {
// dispatch returns true if the action results in a state change,
// and false otherwise. This maps well to what BackHandler expects
// from a callback -- true if handled, false if not handled
return this.dispatch(NavigationActions.back());
}
});
}
this.state = {
nav: this._isStateful()
? Component.router.getStateForAction(this._initialAction)
@@ -124,14 +139,11 @@ export default function createNavigationContainer(Component) {
}
componentDidMount() {
this._isMounted = true;
if (!this._isStateful()) {
return;
}
this.subs = BackHandler.addEventListener('hardwareBackPress', () =>
this.dispatch(NavigationActions.back())
);
Linking.addEventListener('url', this._handleOpenURL);
Linking.getInitialURL().then(url => url && this._handleOpenURL({ url }));
@@ -147,14 +159,14 @@ export default function createNavigationContainer(Component) {
}
componentWillUnmount() {
this._isMounted = false;
Linking.removeEventListener('url', this._handleOpenURL);
this.subs && this.subs.remove();
}
// Per-tick temporary storage for state.nav
dispatch = inputAction => {
const action = NavigationActions.mapDeprecatedActionAndWarn(inputAction);
dispatch = action => {
if (!this._isStateful()) {
return false;
}

View File

@@ -11,6 +11,18 @@ export default function getChildEventSubscriber(addListener, key) {
const willBlurSubscribers = new Set();
const didBlurSubscribers = new Set();
const removeAll = () => {
[
actionSubscribers,
willFocusSubscribers,
didFocusSubscribers,
willBlurSubscribers,
didBlurSubscribers,
].forEach(set => set.clear());
upstreamSubscribers.forEach(subs => subs && subs.remove());
};
const getChildSubscribers = evtName => {
switch (evtName) {
case 'action':
@@ -43,10 +55,6 @@ export default function getChildEventSubscriber(addListener, key) {
// considered blurred
let lastEmittedEvent = 'didBlur';
const cleanup = () => {
upstreamSubscribers.forEach(subs => subs && subs.remove());
};
const upstreamEvents = [
'willFocus',
'didFocus',
@@ -76,7 +84,7 @@ export default function getChildEventSubscriber(addListener, key) {
action,
type: eventName,
};
const isTransitioning = !!state && state.isTransitioning;
const isTransitioning = !!state && !!state.transitioningFromKey;
const previouslyLastEmittedEvent = lastEmittedEvent;
@@ -133,15 +141,18 @@ export default function getChildEventSubscriber(addListener, key) {
})
);
return (eventName, eventHandler) => {
const subscribers = getChildSubscribers(eventName);
if (!subscribers) {
throw new Error(`Invalid event name "${eventName}"`);
}
subscribers.add(eventHandler);
const remove = () => {
subscribers.delete(eventHandler);
};
return { remove };
return {
removeAll,
addListener(eventName, eventHandler) {
const subscribers = getChildSubscribers(eventName);
if (!subscribers) {
throw new Error(`Invalid event name "${eventName}"`);
}
subscribers.add(eventHandler);
const remove = () => {
subscribers.delete(eventHandler);
};
return { remove };
},
};
}

View File

@@ -1,13 +1,13 @@
import React from 'react';
import { Dimensions, Platform, ScrollView } from 'react-native';
import SafeAreaView from 'react-native-safe-area-view';
import createNavigator from './createNavigator';
import createNavigationContainer from '../createNavigationContainer';
import TabRouter from '../routers/TabRouter';
import DrawerRouter from '../routers/DrawerRouter';
import DrawerScreen from '../views/Drawer/DrawerScreen';
import DrawerView from '../views/Drawer/DrawerView';
import DrawerItems from '../views/Drawer/DrawerNavigatorItems';
import SafeAreaView from '../views/SafeAreaView';
// A stack navigators props are the intersection between
// the base navigator props (navgiation, screenProps, etc)
@@ -38,9 +38,6 @@ const DefaultDrawerConfig = {
return Math.min(smallerAxisSize - appBarHeight, maxWidth);
},
contentComponent: defaultContentComponent,
drawerOpenRoute: 'DrawerOpen',
drawerCloseRoute: 'DrawerClose',
drawerToggleRoute: 'DrawerToggle',
drawerPosition: 'left',
drawerBackgroundColor: 'white',
useNativeAnimations: true,
@@ -48,58 +45,25 @@ const DefaultDrawerConfig = {
const DrawerNavigator = (routeConfigs, config = {}) => {
const mergedConfig = { ...DefaultDrawerConfig, ...config };
const {
containerConfig,
drawerWidth,
drawerLockMode,
contentComponent,
contentOptions,
drawerPosition,
useNativeAnimations,
drawerBackgroundColor,
drawerOpenRoute,
drawerCloseRoute,
drawerToggleRoute,
...tabsConfig
order,
paths,
initialRouteName,
backBehavior,
...drawerConfig
} = mergedConfig;
const contentRouter = TabRouter(routeConfigs, tabsConfig);
const drawerRouter = TabRouter(
{
[drawerCloseRoute]: {
screen: createNavigator(contentRouter, routeConfigs, config)(props => (
<DrawerScreen {...props} />
)),
},
[drawerOpenRoute]: {
screen: () => null,
},
[drawerToggleRoute]: {
screen: () => null,
},
},
{
initialRouteName: drawerCloseRoute,
}
);
const routerConfig = {
order,
paths,
initialRouteName,
backBehavior,
};
const navigator = createNavigator(drawerRouter, routeConfigs, config)(
props => (
<DrawerView
{...props}
drawerBackgroundColor={drawerBackgroundColor}
drawerLockMode={drawerLockMode}
useNativeAnimations={useNativeAnimations}
drawerWidth={drawerWidth}
contentComponent={contentComponent}
contentOptions={contentOptions}
drawerPosition={drawerPosition}
drawerOpenRoute={drawerOpenRoute}
drawerCloseRoute={drawerCloseRoute}
drawerToggleRoute={drawerToggleRoute}
/>
)
);
const drawerRouter = DrawerRouter(routeConfigs, routerConfig);
const navigator = createNavigator(DrawerView, drawerRouter, drawerConfig);
return createNavigationContainer(navigator);
};

View File

@@ -1,55 +0,0 @@
import React from 'react';
import createNavigationContainer from '../createNavigationContainer';
import createNavigator from './createNavigator';
import CardStackTransitioner from '../views/CardStack/CardStackTransitioner';
import StackRouter from '../routers/StackRouter';
import NavigationActions from '../NavigationActions';
// A stack navigators props are the intersection between
// the base navigator props (navgiation, screenProps, etc)
// and the view's props
export default (routeConfigMap, stackConfig = {}) => {
const {
initialRouteName,
initialRouteParams,
paths,
headerMode,
mode,
cardStyle,
transitionConfig,
onTransitionStart,
onTransitionEnd,
navigationOptions,
} = stackConfig;
const stackRouterConfig = {
initialRouteName,
initialRouteParams,
paths,
navigationOptions,
};
const router = StackRouter(routeConfigMap, stackRouterConfig);
// Create a navigator with CardStackTransitioner as the view
const navigator = createNavigator(router, routeConfigMap, stackConfig)(
props => (
<CardStackTransitioner
{...props}
headerMode={headerMode}
mode={mode}
cardStyle={cardStyle}
transitionConfig={transitionConfig}
onTransitionStart={onTransitionStart}
onTransitionEnd={(lastTransition, transition) => {
const { state, dispatch } = props.navigation;
dispatch(NavigationActions.completeTransition({ key: state.key }));
onTransitionEnd && onTransitionEnd();
}}
/>
)
);
return createNavigationContainer(navigator);
};

View File

@@ -1,8 +1,14 @@
import React, { Component } from 'react';
import { View } from 'react-native';
import { StyleSheet, View } from 'react-native';
import renderer from 'react-test-renderer';
import StackNavigator from '../StackNavigator';
import StackNavigator from '../createStackNavigator';
const styles = StyleSheet.create({
header: {
opacity: 0.5,
},
});
class HomeScreen extends Component {
static navigationOptions = ({ navigation }) => ({
@@ -10,6 +16,7 @@ class HomeScreen extends Component {
navigation.state.params ? navigation.state.params.user : 'anonymous'
}`,
gesturesEnabled: true,
headerStyle: [{ backgroundColor: 'red' }, styles.header],
});
render() {

View File

@@ -2,7 +2,7 @@ import React, { Component } from 'react';
import { View } from 'react-native';
import renderer from 'react-test-renderer';
import TabNavigator from '../TabNavigator';
import TabNavigator from '../createTabNavigator';
class HomeScreen extends Component {
static navigationOptions = ({ navigation }) => ({

View File

@@ -140,6 +140,7 @@ exports[`DrawerNavigator renders successfully 1`] = `
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"paddingBottom": 0,
@@ -187,6 +188,7 @@ exports[`DrawerNavigator renders successfully 1`] = `
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"backgroundColor": "rgba(0, 0, 0, .04)",
@@ -222,6 +224,7 @@ exports[`DrawerNavigator renders successfully 1`] = `
"color": "#2196f3",
},
undefined,
undefined,
]
}
>

View File

@@ -75,161 +75,109 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
/>
</View>
<View
cardStyle={undefined}
collapsable={undefined}
getScreenDetails={[Function]}
headerMode={undefined}
index={0}
layout={
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"height": 0,
"initHeight": 0,
"initWidth": 0,
"isMeasured": false,
"width": 0,
"backgroundColor": "red",
"borderBottomColor": "#A7A7AA",
"borderBottomWidth": 0.5,
"height": 64,
"opacity": 0.5,
"paddingBottom": 0,
"paddingLeft": 0,
"paddingRight": 0,
"paddingTop": 20,
}
}
leftInterpolator={[Function]}
mode="float"
navigation={
Object {
"addListener": [Function],
"dispatch": [Function],
"goBack": [Function],
"navigate": [Function],
"pop": [Function],
"popToTop": [Function],
"push": [Function],
"replace": [Function],
"setParams": [Function],
"state": Object {
"index": 0,
"isTransitioning": false,
"key": "StackRouterRoot",
"routes": Array [
Object {
"key": "Init-id-0-1",
"routeName": "Home",
},
],
},
}
}
rightInterpolator={[Function]}
router={
Object {
"getActionForPathAndParams": [Function],
"getComponentForRouteName": [Function],
"getComponentForState": [Function],
"getPathAndParamsForState": [Function],
"getScreenConfig": [Function],
"getScreenOptions": [Function],
"getStateForAction": [Function],
}
}
titleInterpolator={[Function]}
transitionConfig={undefined}
>
<View
collapsable={undefined}
onLayout={[Function]}
style={
Object {
"backgroundColor": "#F7F7F7",
"borderBottomColor": "rgba(0, 0, 0, .3)",
"borderBottomWidth": 0.5,
"height": 64,
"paddingBottom": 0,
"paddingLeft": 0,
"paddingRight": 0,
"paddingTop": 20,
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
<View
style={
Object {
"flex": 1,
}
}
>
<View
style={
Object {
"flex": 1,
"bottom": 0,
"flexDirection": "row",
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
>
<View
collapsable={undefined}
pointerEvents="box-none"
style={
Array [
Object {
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
},
Object {
"flexDirection": "row",
},
]
Object {
"alignItems": "center",
"backgroundColor": "transparent",
"bottom": 0,
"flexDirection": "row",
"justifyContent": "center",
"left": 70,
"opacity": 1,
"position": "absolute",
"right": 70,
"top": 0,
}
}
>
<View
<Text
accessibilityTraits="header"
accessible={true}
allowFontScaling={true}
collapsable={undefined}
pointerEvents="box-none"
ellipsizeMode="tail"
numberOfLines={1}
onLayout={[Function]}
style={
Object {
"alignItems": "center",
"backgroundColor": "transparent",
"bottom": 0,
"justifyContent": "center",
"left": 70,
"opacity": 1,
"position": "absolute",
"right": 70,
"top": 0,
"transform": Array [
Object {
"translateX": 0,
},
],
"color": "rgba(0, 0, 0, .9)",
"fontSize": 17,
"fontWeight": "700",
"marginHorizontal": 16,
"textAlign": "center",
}
}
>
<Text
accessibilityTraits="header"
accessible={true}
allowFontScaling={true}
collapsable={undefined}
ellipsizeMode="tail"
numberOfLines={1}
onLayout={[Function]}
style={
Object {
"color": "rgba(0, 0, 0, .9)",
"fontSize": 17,
"fontWeight": "600",
"marginHorizontal": 16,
"textAlign": "center",
}
}
>
Welcome anonymous
</Text>
</View>
<View
collapsable={undefined}
pointerEvents="box-none"
style={
Object {
"alignItems": "center",
"backgroundColor": "transparent",
"bottom": 0,
"justifyContent": "center",
"opacity": 1,
"position": "absolute",
"right": 0,
"top": 0,
}
Welcome anonymous
</Text>
</View>
<View
collapsable={undefined}
pointerEvents="box-none"
style={
Object {
"alignItems": "center",
"backgroundColor": "transparent",
"bottom": 0,
"flexDirection": "row",
"opacity": 1,
"position": "absolute",
"right": 0,
"top": 0,
}
>
<View />
</View>
}
>
<View />
</View>
</View>
</View>
@@ -313,143 +261,91 @@ exports[`StackNavigator renders successfully 1`] = `
/>
</View>
<View
cardStyle={undefined}
collapsable={undefined}
getScreenDetails={[Function]}
headerMode={undefined}
index={0}
layout={
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"height": 0,
"initHeight": 0,
"initWidth": 0,
"isMeasured": false,
"width": 0,
"backgroundColor": "red",
"borderBottomColor": "#A7A7AA",
"borderBottomWidth": 0.5,
"height": 64,
"opacity": 0.5,
"paddingBottom": 0,
"paddingLeft": 0,
"paddingRight": 0,
"paddingTop": 20,
}
}
leftInterpolator={[Function]}
mode="float"
navigation={
Object {
"addListener": [Function],
"dispatch": [Function],
"goBack": [Function],
"navigate": [Function],
"pop": [Function],
"popToTop": [Function],
"push": [Function],
"replace": [Function],
"setParams": [Function],
"state": Object {
"index": 0,
"isTransitioning": false,
"key": "StackRouterRoot",
"routes": Array [
Object {
"key": "Init-id-0-0",
"routeName": "Home",
},
],
},
}
}
rightInterpolator={[Function]}
router={
Object {
"getActionForPathAndParams": [Function],
"getComponentForRouteName": [Function],
"getComponentForState": [Function],
"getPathAndParamsForState": [Function],
"getScreenConfig": [Function],
"getScreenOptions": [Function],
"getStateForAction": [Function],
}
}
titleInterpolator={[Function]}
transitionConfig={undefined}
>
<View
collapsable={undefined}
onLayout={[Function]}
style={
Object {
"backgroundColor": "#F7F7F7",
"borderBottomColor": "rgba(0, 0, 0, .3)",
"borderBottomWidth": 0.5,
"height": 64,
"paddingBottom": 0,
"paddingLeft": 0,
"paddingRight": 0,
"paddingTop": 20,
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
<View
style={
Object {
"flex": 1,
}
}
>
<View
style={
Object {
"flex": 1,
"bottom": 0,
"flexDirection": "row",
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
>
<View
collapsable={undefined}
pointerEvents="box-none"
style={
Array [
Object {
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
},
Object {
"flexDirection": "row",
},
]
Object {
"alignItems": "center",
"backgroundColor": "transparent",
"bottom": 0,
"flexDirection": "row",
"justifyContent": "center",
"left": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"top": 0,
}
}
>
<View
<Text
accessibilityTraits="header"
accessible={true}
allowFontScaling={true}
collapsable={undefined}
pointerEvents="box-none"
ellipsizeMode="tail"
numberOfLines={1}
onLayout={[Function]}
style={
Object {
"alignItems": "center",
"backgroundColor": "transparent",
"bottom": 0,
"justifyContent": "center",
"left": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"top": 0,
"transform": Array [
Object {
"translateX": 0,
},
],
"color": "rgba(0, 0, 0, .9)",
"fontSize": 17,
"fontWeight": "700",
"marginHorizontal": 16,
"textAlign": "center",
}
}
>
<Text
accessibilityTraits="header"
accessible={true}
allowFontScaling={true}
collapsable={undefined}
ellipsizeMode="tail"
numberOfLines={1}
onLayout={[Function]}
style={
Object {
"color": "rgba(0, 0, 0, .9)",
"fontSize": 17,
"fontWeight": "600",
"marginHorizontal": 16,
"textAlign": "center",
}
}
>
Welcome anonymous
</Text>
</View>
Welcome anonymous
</Text>
</View>
</View>
</View>

View File

@@ -55,13 +55,23 @@ exports[`TabNavigator renders successfully 1`] = `
testID={undefined}
>
<View
collapsable={false}
removeClippedSubviews={false}
style={
Object {
"flex": 1,
"overflow": "hidden",
}
}
/>
>
<View
style={
Object {
"flex": 1,
}
}
/>
</View>
</View>
</View>
<View
@@ -71,6 +81,7 @@ exports[`TabNavigator renders successfully 1`] = `
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"backgroundColor": "#F7F7F7",
@@ -136,13 +147,12 @@ exports[`TabNavigator renders successfully 1`] = `
style={
Object {
"alignItems": "center",
"bottom": 0,
"alignSelf": "center",
"height": "100%",
"justifyContent": "center",
"left": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"top": 0,
"width": "100%",
}
}
/>
@@ -151,13 +161,12 @@ exports[`TabNavigator renders successfully 1`] = `
style={
Object {
"alignItems": "center",
"bottom": 0,
"alignSelf": "center",
"height": "100%",
"justifyContent": "center",
"left": 0,
"opacity": 0,
"position": "absolute",
"right": 0,
"top": 0,
"width": "100%",
}
}
/>
@@ -167,6 +176,7 @@ exports[`TabNavigator renders successfully 1`] = `
allowFontScaling={true}
collapsable={undefined}
ellipsizeMode="tail"
numberOfLines={1}
style={
Object {
"backgroundColor": "transparent",

View File

@@ -1,19 +1,81 @@
import React from 'react';
/**
* Creates a navigator based on a router and a view that renders the screens.
*/
export default function createNavigator(router, routeConfigs, navigatorConfig) {
return NavigationView => {
class Navigator extends React.Component {
static router = router;
static navigationOptions = null;
import getChildEventSubscriber from '../getChildEventSubscriber';
import addNavigationHelpers from '../addNavigationHelpers';
render() {
return <NavigationView {...this.props} router={router} />;
}
function createNavigator(NavigatorView, router, navigationConfig) {
class Navigator extends React.Component {
static router = router;
static navigationOptions = null;
childEventSubscribers = {};
// Cleanup subscriptions for routes that no longer exist
componentDidUpdate() {
const activeKeys = this.props.navigation.state.routes.map(r => r.key);
Object.keys(this.childEventSubscribers).forEach(key => {
if (!activeKeys.includes(key)) {
this.childEventSubscribers[key].removeAll();
delete this.childEventSubscribers[key];
}
});
}
return Navigator;
};
// Remove all subscriptions
componentWillUnmount() {
Object.values(this.childEventSubscribers).map(s => s.removeAll());
}
_isRouteFocused = route => () => {
const { state } = this.props.navigation;
const focusedRoute = state.routes[state.index];
return route === focusedRoute;
};
render() {
const { navigation, screenProps } = this.props;
const { dispatch, state, addListener } = navigation;
const { routes } = state;
const descriptors = {};
routes.forEach(route => {
const getComponent = () =>
router.getComponentForRouteName(route.routeName);
if (!this.childEventSubscribers[route.key]) {
this.childEventSubscribers[route.key] = getChildEventSubscriber(
addListener,
route.key
);
}
const childNavigation = addNavigationHelpers({
dispatch,
state: route,
addListener: this.childEventSubscribers[route.key].addListener,
isFocused: this._isRouteFocused.bind(this, route),
});
const options = router.getScreenOptions(childNavigation, screenProps);
descriptors[route.key] = {
key: route.key,
getComponent,
options,
state: route,
navigation: childNavigation,
};
});
return (
<NavigatorView
screenProps={screenProps}
navigation={navigation}
navigationConfig={navigationConfig}
descriptors={descriptors}
/>
);
}
}
return Navigator;
}
export default createNavigator;

View File

@@ -0,0 +1,31 @@
import * as React from 'react';
import createNavigationContainer from '../createNavigationContainer';
import createNavigator from './createNavigator';
import StackView from '../views/StackView/StackView2';
import StackRouter from '../routers/StackRouter';
function createStackNavigator(routeConfigMap, stackConfig = {}) {
const {
initialRouteName,
initialRouteParams,
paths,
navigationOptions,
} = stackConfig;
const stackRouterConfig = {
initialRouteName,
initialRouteParams,
paths,
navigationOptions,
};
const router = StackRouter(routeConfigMap, stackRouterConfig);
// Create a navigator with StackView as the view
const Navigator = createNavigator(StackView, router, stackConfig);
// HOC to provide the navigation prop for the top-level navigator (when the prop is missing)
return createNavigationContainer(Navigator);
}
export default createStackNavigator;

View File

@@ -8,38 +8,13 @@ import TabView from '../views/TabView/TabView';
import TabBarTop from '../views/TabView/TabBarTop';
import TabBarBottom from '../views/TabView/TabBarBottom';
// A tab navigators props are the intersection between
// the base navigator props (navgiation, screenProps, etc)
// and the view's props
const TabNavigator = (routeConfigs, config = {}) => {
// Use the look native to the platform by default
const mergedConfig = { ...TabNavigator.Presets.Default, ...config };
const {
tabBarComponent,
tabBarPosition,
tabBarOptions,
swipeEnabled,
animationEnabled,
configureTransition,
initialLayout,
...tabsConfig
} = mergedConfig;
const tabsConfig = { ...TabNavigator.Presets.Default, ...config };
const router = TabRouter(routeConfigs, tabsConfig);
const navigator = createNavigator(router, routeConfigs, config)(props => (
<TabView
{...props}
tabBarComponent={tabBarComponent}
tabBarPosition={tabBarPosition}
tabBarOptions={tabBarOptions}
swipeEnabled={swipeEnabled}
animationEnabled={animationEnabled}
configureTransition={configureTransition}
initialLayout={initialLayout}
/>
));
const navigator = createNavigator(TabView, router, tabsConfig);
return createNavigationContainer(navigator);
};

View File

@@ -20,10 +20,10 @@ module.exports = {
return require('./navigators/createNavigator').default;
},
get StackNavigator() {
return require('./navigators/StackNavigator').default;
return require('./navigators/createStackNavigator').default;
},
get TabNavigator() {
return require('./navigators/TabNavigator').default;
return require('./navigators/createTabNavigator').default;
},
get DrawerNavigator() {
return require('./navigators/DrawerNavigator').default;
@@ -41,17 +41,14 @@ module.exports = {
get Transitioner() {
return require('./views/Transitioner').default;
},
get CardStackTransitioner() {
return require('./views/CardStack/CardStackTransitioner').default;
get StackView() {
return require('./views/StackView/StackView').default;
},
get CardStack() {
return require('./views/CardStack/CardStack').default;
},
get Card() {
return require('./views/CardStack/Card').default;
get StackViewCard() {
return require('./views/StackView/StackViewCard').default;
},
get SafeAreaView() {
return require('./views/SafeAreaView').default;
return require('react-native-safe-area-view').default;
},
// Header
@@ -88,4 +85,7 @@ module.exports = {
get withNavigation() {
return require('./views/withNavigation').default;
},
get withNavigationFocus() {
return require('./views/withNavigationFocus').default;
},
};

View File

@@ -32,4 +32,7 @@ module.exports = {
get withNavigation() {
return require('./views/withNavigation').default;
},
get withNavigationFocus() {
return require('./views/withNavigationFocus').default;
},
};

View File

@@ -0,0 +1,55 @@
import invariant from '../utils/invariant';
import TabRouter from './TabRouter';
import NavigationActions from '../NavigationActions';
export default (routeConfigs, config = {}) => {
const tabRouter = TabRouter(routeConfigs, config);
return {
...tabRouter,
getStateForAction(action, lastState) {
const state = lastState || {
...tabRouter.getStateForAction(action, undefined),
isDrawerOpen: false,
};
// Handle explicit drawer actions
if (
state.isDrawerOpen &&
action.type === NavigationActions.CLOSE_DRAWER
) {
return {
...state,
isDrawerOpen: false,
};
}
if (
!state.isDrawerOpen &&
action.type === NavigationActions.OPEN_DRAWER
) {
return {
...state,
isDrawerOpen: true,
};
}
if (action.type === NavigationActions.TOGGLE_DRAWER) {
return {
...state,
isDrawerOpen: !state.isDrawerOpen,
};
}
// Fall back on tab router for screen switching logic
const tabState = tabRouter.getStateForAction(action, state);
if (tabState !== null && tabState !== state) {
// If the tabs have changed, make sure to close the drawer
return {
...tabState,
isDrawerOpen: false,
};
}
return state;
},
};
};

View File

@@ -5,7 +5,6 @@ import createConfigGetter from './createConfigGetter';
import getScreenForRouteName from './getScreenForRouteName';
import StateUtils from '../StateUtils';
import validateRouteConfigMap from './validateRouteConfigMap';
import getScreenConfigDeprecated from './getScreenConfigDeprecated';
import invariant from '../utils/invariant';
import { generateKey } from './KeyGenerator';
@@ -51,6 +50,62 @@ export default (routeConfigs, stackConfig = {}) => {
const pathsByRouteNames = { ...stackConfig.paths } || {};
let paths = [];
function getInitialState(action) {
let route = {};
const childRouter = childRouters[action.routeName];
// This is a push-like action, and childRouter will be a router or null if we are responsible for this routeName
if (behavesLikePushAction(action) && childRouter !== undefined) {
let childState = {};
// The router is null for normal leaf routes
if (childRouter !== null) {
const childAction =
action.action || NavigationActions.init({ params: action.params });
childState = childRouter.getStateForAction(childAction);
}
return {
key: 'StackRouterRoot',
transitioningFromKey: null,
index: 0,
routes: [
{
params: action.params,
...childState,
key: action.key || generateKey(),
routeName: action.routeName,
},
],
};
}
if (initialChildRouter) {
route = initialChildRouter.getStateForAction(
NavigationActions.navigate({
routeName: initialRouteName,
params: initialRouteParams,
})
);
}
const params = (route.params || action.params || initialRouteParams) && {
...(route.params || {}),
...(action.params || {}),
...(initialRouteParams || {}),
};
const { initialRouteKey } = stackConfig;
route = {
...route,
...(params ? { params } : {}),
routeName: initialRouteName,
key: action.key || (initialRouteKey || generateKey()),
};
return {
key: 'StackRouterRoot',
transitioningFromKey: false,
index: 0,
routes: [route],
};
}
// Build paths for each route
routeNames.forEach(routeName => {
let pathPattern =
@@ -100,55 +155,12 @@ export default (routeConfigs, stackConfig = {}) => {
getStateForAction(action, state) {
// Set up the initial state if needed
if (!state) {
let route = {};
if (
behavesLikePushAction(action) &&
childRouters[action.routeName] !== undefined
) {
return {
key: 'StackRouterRoot',
isTransitioning: false,
index: 0,
routes: [
{
routeName: action.routeName,
params: action.params,
key: `Init-${generateKey()}`,
},
],
};
}
if (initialChildRouter) {
route = initialChildRouter.getStateForAction(
NavigationActions.navigate({
routeName: initialRouteName,
params: initialRouteParams,
})
);
}
const params = (route.params ||
action.params ||
initialRouteParams) && {
...(route.params || {}),
...(action.params || {}),
...(initialRouteParams || {}),
};
route = {
...route,
routeName: initialRouteName,
key: `Init-${generateKey()}`,
...(params ? { params } : {}),
};
// eslint-disable-next-line no-param-reassign
state = {
key: 'StackRouterRoot',
isTransitioning: false,
index: 0,
routes: [route],
};
return getInitialState(action);
}
const lastRouteKey = state.routes[state.index].key;
// Check if a child scene wants to handle the action as long as it is not a reset to the root stack
// Check if the focused child scene wants to handle the action, as long as
// it is not a reset to the root stack
if (action.type !== NavigationActions.RESET || action.key !== null) {
const keyIndex = action.key
? StateUtils.indexOf(state, action.key)
@@ -171,49 +183,8 @@ export default (routeConfigs, stackConfig = {}) => {
}
}
// Handle pop-to-top behavior. Make sure this happens after children have had a chance to handle the action, so that the inner stack pops to top first.
if (action.type === NavigationActions.POP_TO_TOP) {
if (state.index === 0) {
return {
...state,
};
} else {
return {
...state,
isTransitioning: action.immediate !== true,
index: 0,
routes: [state.routes[0]],
};
}
return state;
}
// Handle replace action
if (action.type === NavigationActions.REPLACE) {
const routeIndex = state.routes.findIndex(r => r.key === action.key);
// Only replace if the key matches one of our routes
if (routeIndex !== -1) {
const childRouter = childRouters[action.routeName];
let childState = {};
if (childRouter) {
const childAction =
action.action ||
NavigationActions.init({ params: action.params });
childState = childRouter.getStateForAction(childAction);
}
const routes = [...state.routes];
routes[routeIndex] = {
params: action.params,
// merge the child state in this order to allow params override
...childState,
key: action.newKey || generateKey(),
routeName: action.routeName,
};
return { ...state, routes };
}
}
// Handle explicit push navigation action. Make sure this happens after children have had a chance to handle the action
// Handle explicit push navigation action. This must happen after the
// focused child router has had a chance to handle the action.
if (
behavesLikePushAction(action) &&
childRouters[action.routeName] !== undefined
@@ -225,6 +196,7 @@ export default (routeConfigs, stackConfig = {}) => {
action.type !== NavigationActions.PUSH || action.key == null,
'StackRouter does not support key on the push action'
);
// With the navigate action, the key may be provided for pushing, or to navigate back to the key
if (action.key) {
const lastRouteIndex = state.routes.findIndex(
@@ -235,7 +207,10 @@ export default (routeConfigs, stackConfig = {}) => {
if (state.index === lastRouteIndex && !action.params) {
return state;
}
const routes = [...state.routes];
// Remove the now unused routes at the tail of the routes array
const routes = state.routes.slice(0, lastRouteIndex + 1);
// Apply params if provided, otherwise leave route identity intact
if (action.params) {
const route = state.routes.find(r => r.key === action.key);
@@ -247,19 +222,19 @@ export default (routeConfigs, stackConfig = {}) => {
},
};
}
// Return state with new index. Change isTransitioning only if index has changed
// Return state with new index. Change transitioningFromKey only if index has changed
return {
...state,
isTransitioning:
transitioningFromKey:
state.index !== lastRouteIndex
? action.immediate !== true
: undefined,
? action.immediate !== true ? lastRouteKey : null
: null,
index: lastRouteIndex,
routes,
};
}
}
const key = action.key || generateKey();
if (childRouter) {
const childAction =
action.action || NavigationActions.init({ params: action.params });
@@ -267,40 +242,32 @@ export default (routeConfigs, stackConfig = {}) => {
params: action.params,
// merge the child state in this order to allow params override
...childRouter.getStateForAction(childAction),
key,
routeName: action.routeName,
key: action.key || generateKey(),
};
} else {
route = {
params: action.params,
key,
routeName: action.routeName,
key: action.key || generateKey(),
};
}
return {
...StateUtils.push(state, route),
isTransitioning: action.immediate !== true,
transitioningFromKey: action.immediate !== true ? lastRouteKey : null,
};
} else if (
action.type === NavigationActions.PUSH &&
childRouters[action.routeName] === undefined
) {
// If we've made it this far with a push action, we return the
// state with a new identity to prevent the action from bubbling
// back up.
return {
...state,
};
}
if (
action.type === NavigationActions.COMPLETE_TRANSITION &&
(action.key == null || action.key === state.key) &&
state.isTransitioning
) {
return {
...state,
isTransitioning: false,
};
}
// Handle navigation to other child routers that are not yet pushed
if (behavesLikePushAction(action)) {
const childRouterNames = Object.keys(childRouters);
@@ -326,16 +293,79 @@ export default (routeConfigs, stackConfig = {}) => {
routeToPush = navigatedChildRoute;
}
if (routeToPush) {
return StateUtils.push(state, {
const route = {
...routeToPush,
key: generateKey(),
routeName: childRouterName,
});
key: action.key || generateKey(),
};
return StateUtils.push(state, route);
}
}
}
}
// Handle pop-to-top behavior. Make sure this happens after children have had a chance to handle the action, so that the inner stack pops to top first.
if (action.type === NavigationActions.POP_TO_TOP) {
// Refuse to handle pop to top if a key is given that doesn't correspond
// to this router
if (action.key && state.key !== action.key) {
return state;
}
// If we're already at the top, then we return the state with a new
// identity so that the action is handled by this router.
if (state.index === 0) {
return {
...state,
};
} else {
return {
...state,
lastRouteKey: action.immediate !== true ? lastRouteKey : null,
index: 0,
routes: [state.routes[0]],
};
}
return state;
}
// Handle replace action
if (action.type === NavigationActions.REPLACE) {
const routeIndex = state.routes.findIndex(r => r.key === action.key);
// Only replace if the key matches one of our routes
if (routeIndex !== -1) {
const childRouter = childRouters[action.routeName];
let childState = {};
if (childRouter) {
const childAction =
action.action ||
NavigationActions.init({ params: action.params });
childState = childRouter.getStateForAction(childAction);
}
const routes = [...state.routes];
routes[routeIndex] = {
params: action.params,
// merge the child state in this order to allow params override
...childState,
routeName: action.routeName,
key: action.newKey || generateKey(),
};
return { ...state, routes };
}
}
// Update transitioning state
if (
action.type === NavigationActions.COMPLETE_TRANSITION &&
(action.key == null || action.key === state.key) &&
state.transitioningFromKey
) {
return {
...state,
transitioningFromKey: null,
};
}
if (action.type === NavigationActions.SET_PARAMS) {
const key = action.key;
const lastRoute = state.routes.find(route => route.key === key);
@@ -363,26 +393,29 @@ export default (routeConfigs, stackConfig = {}) => {
// undefined on either the state or the action
return state;
}
const resetAction = action;
const newStackActions = action.actions;
return {
...state,
routes: resetAction.actions.map(childAction => {
const router = childRouters[childAction.routeName];
routes: newStackActions.map(newStackAction => {
const router = childRouters[newStackAction.routeName];
let childState = {};
if (router) {
return {
...childAction,
...router.getStateForAction(childAction),
routeName: childAction.routeName,
key: generateKey(),
};
const childAction =
newStackAction.action ||
NavigationActions.init({ params: newStackAction.params });
childState = router.getStateForAction(childAction);
}
const route = {
...childAction,
key: generateKey(),
return {
params: newStackAction.params,
...childState,
routeName: newStackAction.routeName,
key: newStackAction.key || generateKey(),
};
delete route.type;
return route;
}),
index: action.index,
};
@@ -408,7 +441,7 @@ export default (routeConfigs, stackConfig = {}) => {
...state,
routes: state.routes.slice(0, backRouteIndex),
index: backRouteIndex - 1,
isTransitioning: immediate !== true,
transitioningFromKey: immediate !== true ? lastRouteKey : null,
};
} else if (
backRouteIndex === 0 &&
@@ -521,7 +554,15 @@ export default (routeConfigs, stackConfig = {}) => {
}
const nextResult = result || {};
const paramName = key.name;
nextResult[paramName] = matchResult;
let decodedMatchResult;
try {
decodedMatchResult = decodeURIComponent(matchResult);
} catch (e) {
// ignore `URIError: malformed URI`
}
nextResult[paramName] = decodedMatchResult || matchResult;
return nextResult;
}, queryParams);
@@ -536,7 +577,5 @@ export default (routeConfigs, stackConfig = {}) => {
routeConfigs,
stackConfig.navigationOptions
),
getScreenConfig: getScreenConfigDeprecated,
};
};

View File

@@ -4,7 +4,13 @@ import createConfigGetter from './createConfigGetter';
import NavigationActions from '../NavigationActions';
import validateRouteConfigMap from './validateRouteConfigMap';
import getScreenConfigDeprecated from './getScreenConfigDeprecated';
function childrenUpdateWithoutSwitchingIndex(actionType) {
return [
NavigationActions.SET_PARAMS,
NavigationActions.COMPLETE_TRANSITION,
].includes(actionType);
}
export default (routeConfigs, config = {}) => {
// Fail fast on invalid route definitions
@@ -23,8 +29,9 @@ export default (routeConfigs, config = {}) => {
paths[routeName] =
typeof routeConfig.path === 'string' ? routeConfig.path : routeName;
tabRouters[routeName] = null;
if (routeConfig.screen && routeConfig.screen.router) {
tabRouters[routeName] = routeConfig.screen.router;
const screen = getScreenForRouteName(routeConfigs, routeName);
if (screen.router) {
tabRouters[routeName] = screen.router;
}
});
if (initialRouteIndex === -1) {
@@ -60,7 +67,7 @@ export default (routeConfigs, config = {}) => {
state = {
routes,
index: initialRouteIndex,
isTransitioning: false,
transitioningFromKey: null,
};
// console.log(`${order.join('-')}: Initial state`, {state});
}
@@ -213,9 +220,13 @@ export default (routeConfigs, config = {}) => {
});
// console.log(`${order.join('-')}: Processed other tabs:`, {lastIndex: state.index, index});
// keep active tab index if action type is SET_PARAMS
index =
action.type === NavigationActions.SET_PARAMS ? state.index : index;
// Nested routers can be updated after switching tabs with actions such as SET_PARAMS
// and COMPLETE_TRANSITION.
// NOTE: This may be problematic with custom routers because we whitelist the actions
// that can be handled by child routers without automatically changing index.
if (childrenUpdateWithoutSwitchingIndex(action.type)) {
index = state.index;
}
if (index !== state.index || routes !== state.routes) {
return {
@@ -228,7 +239,7 @@ export default (routeConfigs, config = {}) => {
},
getComponentForState(state) {
const routeName = order[state.index];
const routeName = state.routes[state.index].routeName;
invariant(
routeName,
`There is no route defined for index ${state.index}. Check that
@@ -311,7 +322,5 @@ export default (routeConfigs, config = {}) => {
routeConfigs,
config.navigationOptions
),
getScreenConfig: getScreenConfigDeprecated,
};
};

View File

@@ -0,0 +1,72 @@
/* eslint react/display-name:0 */
import React from 'react';
import DrawerRouter from '../DrawerRouter';
import NavigationActions from '../../NavigationActions';
const INIT_ACTION = { type: NavigationActions.INIT };
describe('DrawerRouter', () => {
test('Handles basic tab logic', () => {
const ScreenA = () => <div />;
const ScreenB = () => <div />;
const router = DrawerRouter({
Foo: { screen: ScreenA },
Bar: { screen: ScreenB },
});
const state = router.getStateForAction(INIT_ACTION);
const expectedState = {
index: 0,
transitioningFromKey: null,
routes: [
{ key: 'Foo', routeName: 'Foo', params: undefined },
{ key: 'Bar', routeName: 'Bar', params: undefined },
],
isDrawerOpen: false,
};
expect(state).toEqual(expectedState);
const state2 = router.getStateForAction(
{ type: NavigationActions.NAVIGATE, routeName: 'Bar' },
state
);
const expectedState2 = {
index: 1,
transitioningFromKey: null,
routes: [
{ key: 'Foo', routeName: 'Foo', params: undefined },
{ key: 'Bar', routeName: 'Bar', params: undefined },
],
isDrawerOpen: false,
};
expect(state2).toEqual(expectedState2);
expect(router.getComponentForState(expectedState)).toEqual(ScreenA);
expect(router.getComponentForState(expectedState2)).toEqual(ScreenB);
});
test('Drawer opens closes and toggles', () => {
const ScreenA = () => <div />;
const ScreenB = () => <div />;
const router = DrawerRouter({
Foo: { screen: ScreenA },
Bar: { screen: ScreenB },
});
const state = router.getStateForAction(INIT_ACTION);
expect(state.isDrawerOpen).toEqual(false);
const state2 = router.getStateForAction(
{ type: NavigationActions.OPEN_DRAWER },
state
);
expect(state2.isDrawerOpen).toEqual(true);
const state3 = router.getStateForAction(
{ type: NavigationActions.CLOSE_DRAWER },
state2
);
expect(state3.isDrawerOpen).toEqual(false);
const state4 = router.getStateForAction(
{ type: NavigationActions.TOGGLE_DRAWER },
state3
);
expect(state4.isDrawerOpen).toEqual(true);
});
});

View File

@@ -7,13 +7,18 @@ import TabRouter from '../TabRouter';
import NavigationActions from '../../NavigationActions';
import addNavigationHelpers from '../../addNavigationHelpers';
import { _TESTING_ONLY_normalize_keys } from '../KeyGenerator';
beforeEach(() => {
_TESTING_ONLY_normalize_keys();
});
const ROUTERS = {
TabRouter,
StackRouter,
};
const dummyEventSubscriber = (name: string, handler: (*) => void) => ({
const dummyEventSubscriber = (name, handler) => ({
remove: () => {},
});
@@ -105,8 +110,8 @@ test('Handles no-op actions with tabs within stack router', () => {
type: NavigationActions.NAVIGATE,
routeName: 'Qux',
});
expect(state1.routes[0].key).toEqual('Init-id-0-0');
expect(state2.routes[0].key).toEqual('Init-id-0-1');
expect(state1.routes[0].key).toEqual('id-0');
expect(state2.routes[0].key).toEqual('id-1');
state1.routes[0].key = state2.routes[0].key;
expect(state1).toEqual(state2);
const state3 = TestRouter.getStateForAction(
@@ -130,11 +135,11 @@ test('Handles deep action', () => {
const state1 = TestRouter.getStateForAction({ type: NavigationActions.INIT });
const expectedState = {
index: 0,
isTransitioning: false,
transitioningFromKey: false,
key: 'StackRouterRoot',
routes: [
{
key: 'Init-id-0-2',
key: 'id-0',
routeName: 'Bar',
},
],
@@ -174,8 +179,8 @@ test('Supports lazily-evaluated getScreen', () => {
immediate: true,
routeName: 'Qux',
});
expect(state1.routes[0].key).toEqual('Init-id-0-4');
expect(state2.routes[0].key).toEqual('Init-id-0-5');
expect(state1.routes[0].key).toEqual('id-0');
expect(state2.routes[0].key).toEqual('id-1');
state1.routes[0].key = state2.routes[0].key;
expect(state1).toEqual(state2);
const state3 = TestRouter.getStateForAction(
@@ -188,3 +193,60 @@ test('Supports lazily-evaluated getScreen', () => {
);
expect(state2).toEqual(state3);
});
test('Does not switch tab index when TabRouter child handles COMPLETE_NAVIGATION or SET_PARAMS', () => {
const FooStackNavigator = () => <div />;
const BarView = () => <div />;
FooStackNavigator.router = StackRouter({
Foo: {
screen: BarView,
},
Bar: {
screen: BarView,
},
});
const TestRouter = TabRouter({
Zap: { screen: FooStackNavigator },
Zoo: { screen: FooStackNavigator },
});
const state1 = TestRouter.getStateForAction({ type: NavigationActions.INIT });
// Navigate to the second screen in the first tab
const state2 = TestRouter.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Bar',
},
state1
);
// Switch tabs
const state3 = TestRouter.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Zoo',
},
state2
);
const stateAfterCompleteTransition = TestRouter.getStateForAction(
{
type: NavigationActions.COMPLETE_TRANSITION,
key: state2.routes[0].key,
},
state3
);
const stateAfterSetParams = TestRouter.getStateForAction(
{
type: NavigationActions.SET_PARAMS,
key: state1.routes[0].routes[0].key,
params: { key: 'value' },
},
state3
);
expect(stateAfterCompleteTransition.index).toEqual(1);
expect(stateAfterSetParams.index).toEqual(1);
});

View File

@@ -92,7 +92,7 @@ describe('StackRouter', () => {
expect(
router.getComponentForState({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
routes: [
{ key: 'a', routeName: 'foo' },
{ key: 'b', routeName: 'bar' },
@@ -103,7 +103,7 @@ describe('StackRouter', () => {
expect(
router.getComponentForState({
index: 1,
isTransitioning: false,
transitioningFromKey: null,
routes: [
{ key: 'a', routeName: 'foo' },
{ key: 'b', routeName: 'bar' },
@@ -127,7 +127,7 @@ describe('StackRouter', () => {
expect(
router.getComponentForState({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
routes: [
{ key: 'a', routeName: 'foo' },
{ key: 'b', routeName: 'bar' },
@@ -138,7 +138,7 @@ describe('StackRouter', () => {
expect(
router.getComponentForState({
index: 1,
isTransitioning: false,
transitioningFromKey: null,
routes: [
{ key: 'a', routeName: 'foo' },
{ key: 'b', routeName: 'bar' },
@@ -353,9 +353,9 @@ describe('StackRouter', () => {
const initState = TestRouter.getStateForAction(NavigationActions.init());
expect(initState).toEqual({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'StackRouterRoot',
routes: [{ key: 'Init-id-0', routeName: 'foo' }],
routes: [{ key: 'id-0', routeName: 'foo' }],
});
const pushedState = TestRouter.getStateForAction(
NavigationActions.navigate({ routeName: 'qux' }),
@@ -457,6 +457,35 @@ describe('StackRouter', () => {
expect(state3 && state3.routes[1].index).toEqual(0);
});
test('popToTop targets StackRouter by key if specified', () => {
const ChildNavigator = () => <div />;
ChildNavigator.router = StackRouter({
Baz: { screen: () => <div /> },
Qux: { screen: () => <div /> },
});
const router = StackRouter({
Foo: { screen: () => <div /> },
Bar: { screen: ChildNavigator },
});
const state = router.getStateForAction({ type: NavigationActions.INIT });
const state2 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Bar',
},
state
);
const barKey = state2.routes[1].routes[0].key;
const state3 = router.getStateForAction(
{
type: NavigationActions.POP_TO_TOP,
key: state2.key,
},
state2
);
expect(state3 && state3.index).toEqual(0);
});
test('popToTop works as expected', () => {
const TestRouter = StackRouter({
foo: { screen: () => <div /> },
@@ -465,7 +494,7 @@ describe('StackRouter', () => {
const state = {
index: 2,
isTransitioning: false,
transitioningFromKey: null,
routes: [
{ key: 'A', routeName: 'foo' },
{ key: 'B', routeName: 'bar', params: { bazId: '321' } },
@@ -478,7 +507,7 @@ describe('StackRouter', () => {
);
expect(poppedState.routes.length).toBe(1);
expect(poppedState.index).toBe(0);
expect(poppedState.isTransitioning).toBe(true);
expect(poppedState.transitioningFromKey).toBe('C');
const poppedState2 = TestRouter.getStateForAction(
NavigationActions.popToTop(),
poppedState
@@ -490,7 +519,7 @@ describe('StackRouter', () => {
);
expect(poppedImmediatelyState.routes.length).toBe(1);
expect(poppedImmediatelyState.index).toBe(0);
expect(poppedImmediatelyState.isTransitioning).toBe(false);
expect(poppedImmediatelyState.transitioningFromKey).toBe(null);
});
test('Navigate Pushes duplicate routeName', () => {
@@ -513,6 +542,57 @@ describe('StackRouter', () => {
expect(pushedTwiceState.routes[2].routeName).toEqual('bar');
});
test('Navigate from top propagates to any arbitary depth of stacks', () => {
const GrandChildNavigator = () => <div />;
GrandChildNavigator.router = StackRouter({
Quux: { screen: () => <div /> },
Corge: { screen: () => <div /> },
});
const ChildNavigator = () => <div />;
ChildNavigator.router = StackRouter({
Baz: { screen: () => <div /> },
Woo: { screen: () => <div /> },
Qux: { screen: GrandChildNavigator },
});
const Parent = StackRouter({
Foo: { screen: () => <div /> },
Bar: { screen: ChildNavigator },
});
const state = Parent.getStateForAction({ type: NavigationActions.INIT });
const state2 = Parent.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Corge',
},
state
);
expect(state2.index).toEqual(1);
expect(state2.routes[1].index).toEqual(1);
expect(state2.routes[1].routes[1].index).toEqual(1);
expect(state2.routes[1].routes[1].routes[1].routeName).toEqual('Corge');
});
test('Navigate to initial screen is possible', () => {
const TestRouter = StackRouter(
{
foo: { screen: () => <div /> },
bar: { screen: () => <div /> },
},
{ initialRouteKey: 'foo' }
);
const initState = TestRouter.getStateForAction(NavigationActions.init());
const pushedState = TestRouter.getStateForAction(
NavigationActions.navigate({ routeName: 'foo', key: 'foo' }),
initState
);
expect(pushedState.index).toEqual(0);
expect(pushedState.routes[0].routeName).toEqual('foo');
});
test('Navigate with key is idempotent', () => {
const TestRouter = StackRouter({
foo: { screen: () => <div /> },
@@ -553,6 +633,37 @@ describe('StackRouter', () => {
}).toThrow();
});
test('Navigate backwards with key removes leading routes', () => {
const TestRouter = StackRouter({
foo: { screen: () => <div /> },
bar: { screen: () => <div /> },
});
const initState = TestRouter.getStateForAction(NavigationActions.init());
const pushedState = TestRouter.getStateForAction(
NavigationActions.navigate({ routeName: 'bar', key: 'a' }),
initState
);
const pushedTwiceState = TestRouter.getStateForAction(
NavigationActions.navigate({ routeName: 'bar', key: 'b`' }),
pushedState
);
const pushedThriceState = TestRouter.getStateForAction(
NavigationActions.navigate({ routeName: 'foo', key: 'c`' }),
pushedTwiceState
);
expect(pushedThriceState.routes.length).toEqual(4);
const navigatedBackToFirstRouteState = TestRouter.getStateForAction(
NavigationActions.navigate({
routeName: 'foo',
key: pushedThriceState.routes[0].key,
}),
pushedThriceState
);
expect(navigatedBackToFirstRouteState.index).toEqual(0);
expect(navigatedBackToFirstRouteState.routes.length).toEqual(1);
});
test('Handle basic stack logic for plain components', () => {
const FooScreen = () => <div />;
const BarScreen = () => <div />;
@@ -567,11 +678,11 @@ describe('StackRouter', () => {
const state = router.getStateForAction({ type: NavigationActions.INIT });
expect(state).toEqual({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'StackRouterRoot',
routes: [
{
key: 'Init-id-0',
key: 'id-0',
routeName: 'Foo',
},
],
@@ -595,11 +706,11 @@ describe('StackRouter', () => {
);
expect(state3).toEqual({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'StackRouterRoot',
routes: [
{
key: 'Init-id-0',
key: 'id-0',
routeName: 'Foo',
},
],
@@ -662,7 +773,7 @@ describe('StackRouter', () => {
state
);
expect(state2 && state2.index).toEqual(1);
expect(state2 && state2.isTransitioning).toEqual(true);
expect(state2 && state2.transitioningFromKey).toEqual(state.routes[0].key);
const state3 = router.getStateForAction(
{
type: NavigationActions.COMPLETE_TRANSITION,
@@ -670,7 +781,7 @@ describe('StackRouter', () => {
state2
);
expect(state3 && state3.index).toEqual(1);
expect(state3 && state3.isTransitioning).toEqual(false);
expect(state3 && state3.transitioningFromKey).toEqual(null);
});
test('Handle basic stack logic for components with router', () => {
@@ -692,11 +803,11 @@ describe('StackRouter', () => {
const state = router.getStateForAction({ type: NavigationActions.INIT });
expect(state).toEqual({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'StackRouterRoot',
routes: [
{
key: 'Init-id-0',
key: 'id-0',
routeName: 'Foo',
},
],
@@ -720,11 +831,11 @@ describe('StackRouter', () => {
);
expect(state3).toEqual({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'StackRouterRoot',
routes: [
{
key: 'Init-id-0',
key: 'id-0',
routeName: 'Foo',
},
],
@@ -794,11 +905,11 @@ describe('StackRouter', () => {
const state = router.getStateForAction({ type: NavigationActions.INIT });
expect(state).toEqual({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'StackRouterRoot',
routes: [
{
key: 'Init-id-0',
key: 'id-0',
routeName: 'Bar',
},
],
@@ -818,7 +929,7 @@ describe('StackRouter', () => {
const state = router.getStateForAction({ type: NavigationActions.INIT });
expect(state).toEqual({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'StackRouterRoot',
routes: [
{
@@ -907,14 +1018,14 @@ describe('StackRouter', () => {
{
type: NavigationActions.SET_PARAMS,
params: { name: 'foobar' },
key: 'Init-id-0',
key: 'id-0',
},
state
);
expect(state2 && state2.index).toEqual(0);
expect(state2 && state2.routes[0].routes[0].routes).toEqual([
{
key: 'Init-id-0',
key: 'id-0',
routeName: 'Quux',
params: { name: 'foobar' },
},
@@ -1133,6 +1244,126 @@ describe('StackRouter', () => {
]);
});
test('Handles the navigate action with params and nested StackRouter as a first action', () => {
const state = TestStackRouter.getStateForAction({
type: NavigationActions.NAVIGATE,
routeName: 'main',
params: {
code: 'test',
foo: 'bar',
},
action: {
type: NavigationActions.NAVIGATE,
routeName: 'profile',
params: {
id: '4',
code: 'test',
foo: 'bar',
},
action: {
type: NavigationActions.NAVIGATE,
routeName: 'list',
params: {
id: '10259959195',
code: 'test',
foo: 'bar',
},
},
},
});
expect(state).toEqual({
index: 0,
transitioningFromKey: null,
key: 'StackRouterRoot',
routes: [
{
index: 0,
transitioningFromKey: null,
key: 'id-2',
params: { code: 'test', foo: 'bar' },
routeName: 'main',
routes: [
{
index: 0,
transitioningFromKey: null,
key: 'id-1',
params: { code: 'test', foo: 'bar', id: '4' },
routeName: 'profile',
routes: [
{
key: 'id-0',
params: { code: 'test', foo: 'bar', id: '10259959195' },
routeName: 'list',
type: undefined,
},
],
},
],
},
],
});
const state2 = TestStackRouter.getStateForAction({
type: NavigationActions.NAVIGATE,
routeName: 'main',
params: {
code: '',
foo: 'bar',
},
action: {
type: NavigationActions.NAVIGATE,
routeName: 'profile',
params: {
id: '4',
code: '',
foo: 'bar',
},
action: {
type: NavigationActions.NAVIGATE,
routeName: 'list',
params: {
id: '10259959195',
code: '',
foo: 'bar',
},
},
},
});
expect(state2).toEqual({
index: 0,
transitioningFromKey: null,
key: 'StackRouterRoot',
routes: [
{
index: 0,
transitioningFromKey: null,
key: 'id-5',
params: { code: '', foo: 'bar' },
routeName: 'main',
routes: [
{
index: 0,
transitioningFromKey: null,
key: 'id-4',
params: { code: '', foo: 'bar', id: '4' },
routeName: 'profile',
routes: [
{
key: 'id-3',
params: { code: '', foo: 'bar', id: '10259959195' },
routeName: 'list',
type: undefined,
},
],
},
],
},
],
});
});
test('Handles the navigate action with params and nested TabRouter', () => {
const ChildNavigator = () => <div />;
ChildNavigator.router = TabRouter({
@@ -1217,7 +1448,7 @@ describe('StackRouter', () => {
const state = {
index: 0,
isTransitioning: false,
transitioningFromKey: null,
routes: [
{
index: 1,
@@ -1321,38 +1552,26 @@ describe('StackRouter', () => {
}
});
test('Maps old actions (uses "Handles the reset action" test)', () => {
global.console.warn = jest.fn();
const router = StackRouter({
Foo: {
screen: () => <div />,
test('URI encoded string get passed to deep link', () => {
const uri = 'people/2018%2F02%2F07';
const action = TestStackRouter.getActionForPathAndParams(uri);
expect(action).toEqual({
routeName: 'person',
params: {
id: '2018/02/07',
},
Bar: {
screen: () => <div />,
type: NavigationActions.NAVIGATE,
});
const malformedUri = 'people/%E0%A4%A';
const action2 = TestStackRouter.getActionForPathAndParams(malformedUri);
expect(action2).toEqual({
routeName: 'person',
params: {
id: '%E0%A4%A',
},
type: NavigationActions.NAVIGATE,
});
const initAction = NavigationActions.mapDeprecatedActionAndWarn({
type: 'Init',
});
const state = router.getStateForAction(initAction);
const resetAction = NavigationActions.mapDeprecatedActionAndWarn({
type: 'Reset',
actions: [
{ type: 'Navigate', routeName: 'Foo', params: { bar: '42' } },
{ type: 'Navigate', routeName: 'Bar' },
],
index: 1,
});
const state2 = router.getStateForAction(resetAction, state);
expect(state2 && state2.index).toEqual(1);
expect(state2 && state2.routes[0].params).toEqual({ bar: '42' });
expect(state2 && state2.routes[0].routeName).toEqual('Foo');
expect(state2 && state2.routes[1].routeName).toEqual('Bar');
expect(console.warn).toBeCalledWith(
expect.stringContaining(
"The action type 'Init' has been renamed to 'Navigation/INIT'"
)
);
});
test('Querystring params get passed to nested deep link', () => {
@@ -1445,10 +1664,12 @@ test('Handles deep navigate completion action', () => {
},
state
);
expect(state2 && state2.index).toEqual(0);
expect(state2 && state2.isTransitioning).toEqual(false);
expect(state2 && state2.routes[0].index).toEqual(1);
expect(state2 && state2.routes[0].isTransitioning).toEqual(true);
expect(state2.index).toEqual(0);
expect(state2.transitioningFromKey).toEqual(null);
expect(state2.routes[0].index).toEqual(1);
expect(state2.routes[0].transitioningFromKey).toEqual(
state.routes[0].routes[state.routes[0].index].key
);
expect(!!key).toEqual(true);
const state3 = router.getStateForAction(
{
@@ -1456,8 +1677,8 @@ test('Handles deep navigate completion action', () => {
},
state2
);
expect(state3 && state3.index).toEqual(0);
expect(state3 && state3.isTransitioning).toEqual(false);
expect(state3 && state3.routes[0].index).toEqual(1);
expect(state3 && state3.routes[0].isTransitioning).toEqual(false);
expect(state3.index).toEqual(0);
expect(state3.transitioningFromKey).toEqual(null);
expect(state3.routes[0].index).toEqual(1);
expect(state3.routes[0].transitioningFromKey).toEqual(null);
});

View File

@@ -593,29 +593,6 @@ describe('TabRouter', () => {
expect(path).toEqual('f/Baz');
});
test('Maps old actions (uses "getStateForAction returns null when navigating to same tab" test)', () => {
global.console.warn = jest.fn();
const router = TabRouter(
{ Foo: BareLeafRouteConfig, Bar: BareLeafRouteConfig },
{ initialRouteName: 'Bar' }
);
const initAction = NavigationActions.mapDeprecatedActionAndWarn({
type: 'Init',
});
const state = router.getStateForAction(initAction);
const navigateAction = NavigationActions.mapDeprecatedActionAndWarn({
type: 'Navigate',
routeName: 'Bar',
});
const state2 = router.getStateForAction(navigateAction, state);
expect(state2).toEqual(null);
expect(console.warn).toBeCalledWith(
expect.stringContaining(
"The action type 'Init' has been renamed to 'Navigation/INIT'"
)
);
});
test('Can navigate to other tab (no router) with params', () => {
const ScreenA = () => <div />;
const ScreenB = () => <div />;

View File

@@ -0,0 +1,37 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`validateRouteConfigMap Fails if both screen and getScreen are defined 1`] = `"Route 'Home' should declare a screen or a getScreen, not both."`;
exports[`validateRouteConfigMap Fails on bad object 1`] = `
"The component for route 'Home' must be a React component. For example:
import MyScreen from './MyScreen';
...
Home: MyScreen,
}
You can also use a navigator:
import MyNavigator from './MyNavigator';
...
Home: MyNavigator,
}"
`;
exports[`validateRouteConfigMap Fails on empty bare screen 1`] = `
"The component for route 'Home' must be a React component. For example:
import MyScreen from './MyScreen';
...
Home: MyScreen,
}
You can also use a navigator:
import MyNavigator from './MyNavigator';
...
Home: MyNavigator,
}"
`;
exports[`validateRouteConfigMap Fails on empty config 1`] = `"Please specify at least one route when configuring a navigator."`;

View File

@@ -175,23 +175,3 @@ test('should throw if the route does not exist', () => {
"There is no route defined for key Settings.\nMust be one of: 'Home'"
);
});
test('should throw if the screen is not defined under the route config', () => {
/* eslint-disable react/no-multi-comp */
const getScreenOptions = createConfigGetter({
Home: {},
});
const routes = [{ key: 'B', routeName: 'Home' }];
expect(() =>
getScreenOptions(
addNavigationHelpers({
state: routes[0],
dispatch: () => false,
addListener: dummyEventSubscriber,
})
)
).toThrowError('Route Home must define a screen or a getScreen.');
});

View File

@@ -13,9 +13,19 @@ ProfileNavigator.router = StackRouter({
});
describe('validateRouteConfigMap', () => {
test('Fails on empty bare screen', () => {
const invalidMap = {
Home: undefined,
};
expect(() =>
validateRouteConfigMap(invalidMap)
).toThrowErrorMatchingSnapshot();
});
test('Fails on empty config', () => {
const invalidMap = {};
expect(() => validateRouteConfigMap(invalidMap)).toThrow();
expect(() =>
validateRouteConfigMap(invalidMap)
).toThrowErrorMatchingSnapshot();
});
test('Fails on bad object', () => {
const invalidMap = {
@@ -23,7 +33,9 @@ describe('validateRouteConfigMap', () => {
foo: 'bar',
},
};
expect(() => validateRouteConfigMap(invalidMap)).toThrow();
expect(() =>
validateRouteConfigMap(invalidMap)
).toThrowErrorMatchingSnapshot();
});
test('Fails if both screen and getScreen are defined', () => {
const invalidMap = {
@@ -32,17 +44,17 @@ describe('validateRouteConfigMap', () => {
getScreen: () => ListScreen,
},
};
expect(() => validateRouteConfigMap(invalidMap)).toThrow();
expect(() =>
validateRouteConfigMap(invalidMap)
).toThrowErrorMatchingSnapshot();
});
test('Succeeds on a valid config', () => {
const invalidMap = {
const validMap = {
Home: {
screen: ProfileNavigator,
},
Chat: {
screen: ListScreen,
},
Chat: ListScreen,
};
validateRouteConfigMap(invalidMap);
validateRouteConfigMap(validMap);
});
});

View File

@@ -1,9 +1,7 @@
import invariant from '../utils/invariant';
import getScreenForRouteName from './getScreenForRouteName';
import addNavigationHelpers from '../addNavigationHelpers';
import validateScreenOptions from './validateScreenOptions';
import getChildEventSubscriber from '../getChildEventSubscriber';
function applyConfig(configurer, navigationOptions, configProps) {
if (typeof configurer === 'function') {
@@ -38,28 +36,6 @@ export default (routeConfigs, navigatorScreenConfig) => (
const Component = getScreenForRouteName(routeConfigs, route.routeName);
let outputConfig = {};
const router = Component.router;
if (router) {
const { routes, index } = route;
if (!route || !routes || index == null) {
throw new Error(
`Expect nav state to have routes and index, ${JSON.stringify(route)}`
);
}
const childRoute = routes[index];
const childNavigation = addNavigationHelpers({
state: childRoute,
dispatch,
addListener: getChildEventSubscriber(
navigation.addListener,
childRoute.key
),
});
outputConfig = router.getScreenOptions(childNavigation, screenProps);
}
const routeConfig = routeConfigs[route.routeName];
const routeScreenConfig = routeConfig.navigationOptions;
@@ -67,11 +43,7 @@ export default (routeConfigs, navigatorScreenConfig) => (
const configOptions = { navigation, screenProps: screenProps || {} };
outputConfig = applyConfig(
navigatorScreenConfig,
outputConfig,
configOptions
);
let outputConfig = applyConfig(navigatorScreenConfig, {}, configOptions);
outputConfig = applyConfig(
componentScreenConfig,
outputConfig,

View File

@@ -1,7 +0,0 @@
import invariant from '../utils/invariant';
export default () =>
invariant(
false,
'`getScreenConfig` has been replaced with `getScreenOptions`'
);

View File

@@ -32,5 +32,5 @@ export default function getScreenForRouteName(routeConfigs, routeName) {
return screen;
}
throw new Error(`Route ${routeName} must define a screen or a getScreen.`);
return routeConfig;
}

View File

@@ -13,46 +13,44 @@ function validateRouteConfigMap(routeConfigs) {
routeNames.forEach(routeName => {
const routeConfig = routeConfigs[routeName];
if (!routeConfig.screen && !routeConfig.getScreen) {
throw new Error(
`Route '${routeName}' should declare a screen. ` +
'For example:\n\n' +
"import MyScreen from './MyScreen';\n" +
'...\n' +
`${routeName}: {\n` +
' screen: MyScreen,\n' +
'}'
);
} else if (routeConfig.screen && routeConfig.getScreen) {
throw new Error(
`Route '${routeName}' should declare a screen or ` +
'a getScreen, not both.'
);
}
const screenComponent = getScreenComponent(routeConfig);
if (
routeConfig.screen &&
typeof routeConfig.screen !== 'function' &&
typeof routeConfig.screen !== 'string'
!screenComponent ||
(typeof screenComponent !== 'function' &&
typeof screenComponent !== 'string' &&
!routeConfig.getScreen)
) {
throw new Error(
`The component for route '${routeName}' must be a ` +
'React component. For example:\n\n' +
"import MyScreen from './MyScreen';\n" +
'...\n' +
`${routeName}: {\n` +
' screen: MyScreen,\n' +
`${routeName}: MyScreen,\n` +
'}\n\n' +
'You can also use a navigator:\n\n' +
"import MyNavigator from './MyNavigator';\n" +
'...\n' +
`${routeName}: {\n` +
' screen: MyNavigator,\n' +
`${routeName}: MyNavigator,\n` +
'}'
);
}
if (routeConfig.screen && routeConfig.getScreen) {
throw new Error(
`Route '${routeName}' should declare a screen or ` +
'a getScreen, not both.'
);
}
});
}
function getScreenComponent(routeConfig) {
if (!routeConfig) {
return null;
}
return routeConfig.screen ? routeConfig.screen : routeConfig;
}
export default validateRouteConfigMap;

View File

@@ -1,435 +0,0 @@
import React from 'react';
import clamp from 'clamp';
import {
Animated,
StyleSheet,
PanResponder,
Platform,
View,
I18nManager,
Easing,
} from 'react-native';
import Card from './Card';
import Header from '../Header/Header';
import NavigationActions from '../../NavigationActions';
import addNavigationHelpers from '../../addNavigationHelpers';
import getChildEventSubscriber from '../../getChildEventSubscriber';
import SceneView from '../SceneView';
import TransitionConfigs from './TransitionConfigs';
const emptyFunction = () => {};
const EaseInOut = Easing.inOut(Easing.ease);
/**
* The max duration of the card animation in milliseconds after released gesture.
* The actual duration should be always less then that because the rest distance
* is always less then the full distance of the layout.
*/
const ANIMATION_DURATION = 500;
/**
* The gesture distance threshold to trigger the back behavior. For instance,
* `1/2` means that moving greater than 1/2 of the width of the screen will
* trigger a back action
*/
const POSITION_THRESHOLD = 1 / 2;
/**
* The threshold (in pixels) to start the gesture action.
*/
const RESPOND_THRESHOLD = 20;
/**
* The distance of touch start from the edge of the screen where the gesture will be recognized
*/
const GESTURE_RESPONSE_DISTANCE_HORIZONTAL = 25;
const GESTURE_RESPONSE_DISTANCE_VERTICAL = 135;
const animatedSubscribeValue = animatedValue => {
if (!animatedValue.__isNative) {
return;
}
if (Object.keys(animatedValue._listeners).length === 0) {
animatedValue.addListener(emptyFunction);
}
};
class CardStack extends React.Component {
/**
* Used to identify the starting point of the position when the gesture starts, such that it can
* be updated according to its relative position. This means that a card can effectively be
* "caught"- If a gesture starts while a card is animating, the card does not jump into a
* corresponding location for the touch.
*/
_gestureStartValue = 0;
// tracks if a touch is currently happening
_isResponding = false;
/**
* immediateIndex is used to represent the expected index that we will be on after a
* transition. To achieve a smooth animation when swiping back, the action to go back
* doesn't actually fire until the transition completes. The immediateIndex is used during
* the transition so that gestures can be handled correctly. This is a work-around for
* cases when the user quickly swipes back several times.
*/
_immediateIndex = null;
_screenDetails = {};
componentWillReceiveProps(props) {
if (props.screenProps !== this.props.screenProps) {
this._screenDetails = {};
}
props.scenes.forEach(newScene => {
if (
this._screenDetails[newScene.key] &&
this._screenDetails[newScene.key].state !== newScene.route
) {
this._screenDetails[newScene.key] = null;
}
});
}
_getScreenDetails = scene => {
const { screenProps, navigation, router } = this.props;
let screenDetails = this._screenDetails[scene.key];
if (!screenDetails || screenDetails.state !== scene.route) {
const screenNavigation = addNavigationHelpers({
dispatch: navigation.dispatch,
state: scene.route,
addListener: getChildEventSubscriber(
navigation.addListener,
scene.route.key
),
});
screenDetails = {
state: scene.route,
navigation: screenNavigation,
options: router.getScreenOptions(screenNavigation, screenProps),
};
this._screenDetails[scene.key] = screenDetails;
}
return screenDetails;
};
_renderHeader(scene, headerMode) {
const { header } = this._getScreenDetails(scene).options;
if (typeof header !== 'undefined' && typeof header !== 'function') {
return header;
}
const renderHeader = header || (props => <Header {...props} />);
const {
headerLeftInterpolator,
headerTitleInterpolator,
headerRightInterpolator,
} = this._getTransitionConfig();
const { mode, ...passProps } = this.props;
return renderHeader({
...passProps,
scene,
mode: headerMode,
getScreenDetails: this._getScreenDetails,
leftInterpolator: headerLeftInterpolator,
titleInterpolator: headerTitleInterpolator,
rightInterpolator: headerRightInterpolator,
});
}
// eslint-disable-next-line class-methods-use-this
_animatedSubscribe(props) {
// Hack to make this work with native driven animations. We add a single listener
// so the JS value of the following animated values gets updated. We rely on
// some Animated private APIs and not doing so would require using a bunch of
// value listeners but we'd have to remove them to not leak and I'm not sure
// when we'd do that with the current structure we have. `stopAnimation` callback
// is also broken with native animated values that have no listeners so if we
// want to remove this we have to fix this too.
animatedSubscribeValue(props.layout.width);
animatedSubscribeValue(props.layout.height);
animatedSubscribeValue(props.position);
}
_reset(resetToIndex, duration) {
Animated.timing(this.props.position, {
toValue: resetToIndex,
duration,
easing: EaseInOut,
useNativeDriver: this.props.position.__isNative,
}).start();
}
_goBack(backFromIndex, duration) {
const { navigation, position, scenes } = this.props;
const toValue = Math.max(backFromIndex - 1, 0);
// set temporary index for gesture handler to respect until the action is
// dispatched at the end of the transition.
this._immediateIndex = toValue;
Animated.timing(position, {
toValue,
duration,
easing: EaseInOut,
useNativeDriver: position.__isNative,
}).start(() => {
this._immediateIndex = null;
const backFromScene = scenes.find(s => s.index === toValue + 1);
if (!this._isResponding && backFromScene) {
navigation.dispatch(
NavigationActions.back({
key: backFromScene.route.key,
immediate: true,
})
);
}
});
}
render() {
let floatingHeader = null;
const headerMode = this._getHeaderMode();
if (headerMode === 'float') {
floatingHeader = this._renderHeader(this.props.scene, headerMode);
}
const { navigation, position, layout, scene, scenes, mode } = this.props;
const { index } = navigation.state;
const isVertical = mode === 'modal';
const { options } = this._getScreenDetails(scene);
const gestureDirectionInverted = options.gestureDirection === 'inverted';
const responder = PanResponder.create({
onPanResponderTerminate: () => {
this._isResponding = false;
this._reset(index, 0);
},
onPanResponderGrant: () => {
position.stopAnimation(value => {
this._isResponding = true;
this._gestureStartValue = value;
});
},
onMoveShouldSetPanResponder: (event, gesture) => {
if (index !== scene.index) {
return false;
}
const immediateIndex =
this._immediateIndex == null ? index : this._immediateIndex;
const currentDragDistance = gesture[isVertical ? 'dy' : 'dx'];
const currentDragPosition =
event.nativeEvent[isVertical ? 'pageY' : 'pageX'];
const axisLength = isVertical
? layout.height.__getValue()
: layout.width.__getValue();
const axisHasBeenMeasured = !!axisLength;
// Measure the distance from the touch to the edge of the screen
const screenEdgeDistance = gestureDirectionInverted
? axisLength - (currentDragPosition - currentDragDistance)
: currentDragPosition - currentDragDistance;
// Compare to the gesture distance relavant to card or modal
const {
gestureResponseDistance: userGestureResponseDistance = {},
} = this._getScreenDetails(scene).options;
const gestureResponseDistance = isVertical
? userGestureResponseDistance.vertical ||
GESTURE_RESPONSE_DISTANCE_VERTICAL
: userGestureResponseDistance.horizontal ||
GESTURE_RESPONSE_DISTANCE_HORIZONTAL;
// GESTURE_RESPONSE_DISTANCE is about 25 or 30. Or 135 for modals
if (screenEdgeDistance > gestureResponseDistance) {
// Reject touches that started in the middle of the screen
return false;
}
const hasDraggedEnough =
Math.abs(currentDragDistance) > RESPOND_THRESHOLD;
const isOnFirstCard = immediateIndex === 0;
const shouldSetResponder =
hasDraggedEnough && axisHasBeenMeasured && !isOnFirstCard;
return shouldSetResponder;
},
onPanResponderMove: (event, gesture) => {
// Handle the moving touches for our granted responder
const startValue = this._gestureStartValue;
const axis = isVertical ? 'dy' : 'dx';
const axisDistance = isVertical
? layout.height.__getValue()
: layout.width.__getValue();
const currentValue =
(I18nManager.isRTL && axis === 'dx') !== gestureDirectionInverted
? startValue + gesture[axis] / axisDistance
: startValue - gesture[axis] / axisDistance;
const value = clamp(index - 1, currentValue, index);
position.setValue(value);
},
onPanResponderTerminationRequest: () =>
// Returning false will prevent other views from becoming responder while
// the navigation view is the responder (mid-gesture)
false,
onPanResponderRelease: (event, gesture) => {
if (!this._isResponding) {
return;
}
this._isResponding = false;
const immediateIndex =
this._immediateIndex == null ? index : this._immediateIndex;
// Calculate animate duration according to gesture speed and moved distance
const axisDistance = isVertical
? layout.height.__getValue()
: layout.width.__getValue();
const movementDirection = gestureDirectionInverted ? -1 : 1;
const movedDistance =
movementDirection * gesture[isVertical ? 'dy' : 'dx'];
const gestureVelocity =
movementDirection * gesture[isVertical ? 'vy' : 'vx'];
const defaultVelocity = axisDistance / ANIMATION_DURATION;
const velocity = Math.max(Math.abs(gestureVelocity), defaultVelocity);
const resetDuration = gestureDirectionInverted
? (axisDistance - movedDistance) / velocity
: movedDistance / velocity;
const goBackDuration = gestureDirectionInverted
? movedDistance / velocity
: (axisDistance - movedDistance) / velocity;
// To asyncronously get the current animated value, we need to run stopAnimation:
position.stopAnimation(value => {
// If the speed of the gesture release is significant, use that as the indication
// of intent
if (gestureVelocity < -0.5) {
this._reset(immediateIndex, resetDuration);
return;
}
if (gestureVelocity > 0.5) {
this._goBack(immediateIndex, goBackDuration);
return;
}
// Then filter based on the distance the screen was moved. Over a third of the way swiped,
// and the back will happen.
if (value <= index - POSITION_THRESHOLD) {
this._goBack(immediateIndex, goBackDuration);
} else {
this._reset(immediateIndex, resetDuration);
}
});
},
});
const gesturesEnabled =
typeof options.gesturesEnabled === 'boolean'
? options.gesturesEnabled
: Platform.OS === 'ios';
const handlers = gesturesEnabled ? responder.panHandlers : {};
const containerStyle = [
styles.container,
this._getTransitionConfig().containerStyle,
];
return (
<View {...handlers} style={containerStyle}>
<View style={styles.scenes}>
{scenes.map(s => this._renderCard(s))}
</View>
{floatingHeader}
</View>
);
}
_getHeaderMode() {
if (this.props.headerMode) {
return this.props.headerMode;
}
if (Platform.OS === 'android' || this.props.mode === 'modal') {
return 'screen';
}
return 'float';
}
_renderInnerScene(SceneComponent, scene) {
const { navigation } = this._getScreenDetails(scene);
const { screenProps } = this.props;
const headerMode = this._getHeaderMode();
if (headerMode === 'screen') {
return (
<View style={styles.container}>
<View style={{ flex: 1 }}>
<SceneView
screenProps={screenProps}
navigation={navigation}
component={SceneComponent}
/>
</View>
{this._renderHeader(scene, headerMode)}
</View>
);
}
return (
<SceneView
screenProps={this.props.screenProps}
navigation={navigation}
component={SceneComponent}
/>
);
}
_getTransitionConfig = () => {
const isModal = this.props.mode === 'modal';
return TransitionConfigs.getTransitionConfig(
this.props.transitionConfig,
{},
{},
isModal
);
};
_renderCard = scene => {
const { screenInterpolator } = this._getTransitionConfig();
const style =
screenInterpolator && screenInterpolator({ ...this.props, scene });
const SceneComponent = this.props.router.getComponentForRouteName(
scene.route.routeName
);
return (
<Card
{...this.props}
key={`card_${scene.key}`}
style={[style, this.props.cardStyle]}
scene={scene}
>
{this._renderInnerScene(SceneComponent, scene)}
</Card>
);
};
}
const styles = StyleSheet.create({
container: {
flex: 1,
// Header is physically rendered after scenes so that Header won't be
// covered by the shadows of the scenes.
// That said, we'd have use `flexDirection: 'column-reverse'` to move
// Header above the scenes.
flexDirection: 'column-reverse',
},
scenes: {
flex: 1,
},
});
export default CardStack;

View File

@@ -1,79 +0,0 @@
import React from 'react';
import { NativeModules } from 'react-native';
import CardStack from './CardStack';
import CardStackStyleInterpolator from './CardStackStyleInterpolator';
import Transitioner from '../Transitioner';
import TransitionConfigs from './TransitionConfigs';
const NativeAnimatedModule =
NativeModules && NativeModules.NativeAnimatedModule;
class CardStackTransitioner extends React.Component {
static defaultProps = {
mode: 'card',
};
render() {
return (
<Transitioner
configureTransition={this._configureTransition}
navigation={this.props.navigation}
render={this._render}
onTransitionStart={this.props.onTransitionStart}
onTransitionEnd={this.props.onTransitionEnd}
/>
);
}
_configureTransition = (
// props for the new screen
transitionProps,
// props for the old screen
prevTransitionProps
) => {
const isModal = this.props.mode === 'modal';
// Copy the object so we can assign useNativeDriver below
const transitionSpec = {
...TransitionConfigs.getTransitionConfig(
this.props.transitionConfig,
transitionProps,
prevTransitionProps,
isModal
).transitionSpec,
};
if (
!!NativeAnimatedModule &&
// Native animation support also depends on the transforms used:
CardStackStyleInterpolator.canUseNativeDriver()
) {
// Internal undocumented prop
transitionSpec.useNativeDriver = true;
}
return transitionSpec;
};
_render = props => {
const {
screenProps,
headerMode,
mode,
router,
cardStyle,
transitionConfig,
} = this.props;
return (
<CardStack
screenProps={screenProps}
headerMode={headerMode}
mode={mode}
router={router}
cardStyle={cardStyle}
transitionConfig={transitionConfig}
{...props}
/>
);
};
}
export default CardStackTransitioner;

View File

@@ -1,95 +0,0 @@
import React from 'react';
import invariant from '../../utils/invariant';
import AnimatedValueSubscription from '../AnimatedValueSubscription';
const MIN_POSITION_OFFSET = 0.01;
/**
* Create a higher-order component that automatically computes the
* `pointerEvents` property for a component whenever navigation position
* changes.
*/
export default function create(Component) {
class Container extends React.Component {
constructor(props, context) {
super(props, context);
this._pointerEvents = this._computePointerEvents();
}
componentWillMount() {
this._onPositionChange = this._onPositionChange.bind(this);
this._onComponentRef = this._onComponentRef.bind(this);
}
componentDidMount() {
this._bindPosition(this.props);
}
componentWillUnmount() {
this._positionListener && this._positionListener.remove();
}
componentWillReceiveProps(nextProps) {
this._bindPosition(nextProps);
}
render() {
this._pointerEvents = this._computePointerEvents();
return (
<Component
{...this.props}
pointerEvents={this._pointerEvents}
onComponentRef={this._onComponentRef}
/>
);
}
_onComponentRef(component) {
this._component = component;
if (component) {
invariant(
typeof component.setNativeProps === 'function',
'component must implement method `setNativeProps`'
);
}
}
_bindPosition(props) {
this._positionListener && this._positionListener.remove();
this._positionListener = new AnimatedValueSubscription(
props.position,
this._onPositionChange
);
}
_onPositionChange() {
if (this._component) {
const pointerEvents = this._computePointerEvents();
if (this._pointerEvents !== pointerEvents) {
this._pointerEvents = pointerEvents;
this._component.setNativeProps({ pointerEvents });
}
}
}
_computePointerEvents() {
const { navigation, position, scene } = this.props;
if (scene.isStale || navigation.state.index !== scene.index) {
// The scene isn't focused.
return scene.index > navigation.state.index ? 'box-only' : 'none';
}
const offset = position.__getAnimatedValue() - navigation.state.index;
if (Math.abs(offset) > MIN_POSITION_OFFSET) {
// The positon is still away from scene's index.
// Scene's children should not receive touches until the position
// is close enough to scene's index.
return 'box-only';
}
return 'auto';
}
}
return Container;
}

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { View, Text, Platform, StyleSheet } from 'react-native';
import SafeAreaView from 'react-native-safe-area-view';
import SafeAreaView from '../SafeAreaView';
import TouchableItem from '../TouchableItem';
/**
@@ -21,6 +21,8 @@ const DrawerNavigatorItems = ({
itemsContainerStyle,
itemStyle,
labelStyle,
activeLabelStyle,
inactiveLabelStyle,
iconContainerStyle,
drawerPosition,
}) => (
@@ -34,6 +36,7 @@ const DrawerNavigatorItems = ({
const scene = { route, index, focused, tintColor: color };
const icon = renderIcon(scene);
const label = getLabel(scene);
const extraLabelStyle = focused ? activeLabelStyle : inactiveLabelStyle;
return (
<TouchableItem
key={route.key}
@@ -63,7 +66,9 @@ const DrawerNavigatorItems = ({
</View>
) : null}
{typeof label === 'string' ? (
<Text style={[styles.label, { color }, labelStyle]}>
<Text
style={[styles.label, { color }, labelStyle, extraLabelStyle]}
>
{label}
</Text>
) : (

View File

@@ -1,30 +1,24 @@
import React from 'react';
import SceneView from '../SceneView';
import withCachedChildNavigation from '../../withCachedChildNavigation';
/**
* Component that renders the child screen of the drawer.
*/
class DrawerScreen extends React.PureComponent {
render() {
const {
router,
navigation,
childNavigationProps,
screenProps,
} = this.props;
const { descriptors, navigation, screenProps } = this.props;
const { routes, index } = navigation.state;
const childNavigation = childNavigationProps[routes[index].key];
const Content = router.getComponentForRouteName(routes[index].routeName);
const descriptor = descriptors[routes[index].key];
const Content = descriptor.getComponent();
return (
<SceneView
screenProps={screenProps}
component={Content}
navigation={childNavigation}
navigation={descriptor.navigation}
/>
);
}
}
export default withCachedChildNavigation(DrawerScreen);
export default DrawerScreen;

View File

@@ -1,34 +1,22 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import SafeAreaView from 'react-native-safe-area-view';
import withCachedChildNavigation from '../../withCachedChildNavigation';
import NavigationActions from '../../NavigationActions';
import invariant from '../../utils/invariant';
import SafeAreaView from '../SafeAreaView';
/**
* Component that renders the sidebar screen of the drawer.
*/
class DrawerSidebar extends React.PureComponent {
_getScreenOptions = routeKey => {
const DrawerScreen = this.props.router.getComponentForRouteName(
'DrawerClose'
);
const descriptor = this.props.descriptors[routeKey];
invariant(
DrawerScreen.router,
'NavigationComponent with routeName DrawerClose should be a Navigator'
);
const { [routeKey]: childNavigation } = this.props.childNavigationProps;
return DrawerScreen.router.getScreenOptions(
childNavigation.state.index !== undefined // if the child screen is a StackRouter then always show the screen options of its first screen (see #1914)
? {
...childNavigation,
state: { ...childNavigation.state, index: 0 },
}
: childNavigation,
this.props.screenProps
descriptor.options,
'Cannot access screen descriptor options from drawer sidebar'
);
return descriptor.options;
};
_getLabel = ({ focused, tintColor, route }) => {
@@ -57,7 +45,6 @@ class DrawerSidebar extends React.PureComponent {
};
_onItemPress = ({ route, focused }) => {
this.props.navigation.navigate('DrawerClose');
if (!focused) {
let subAction;
// if the child screen is a StackRouter then always navigate to its first screen (see #1914)
@@ -87,6 +74,7 @@ class DrawerSidebar extends React.PureComponent {
<ContentComponent
{...this.props.contentOptions}
navigation={this.props.navigation}
descriptors={this.props.descriptors}
items={state.routes}
activeItemKey={
state.routes[state.index] ? state.routes[state.index].key : null
@@ -95,7 +83,6 @@ class DrawerSidebar extends React.PureComponent {
getLabel={this._getLabel}
renderIcon={this._renderIcon}
onItemPress={this._onItemPress}
router={this.props.router}
drawerPosition={this.props.drawerPosition}
/>
</View>
@@ -103,7 +90,7 @@ class DrawerSidebar extends React.PureComponent {
}
}
export default withCachedChildNavigation(DrawerSidebar);
export default DrawerSidebar;
const styles = StyleSheet.create({
container: {

View File

@@ -4,7 +4,7 @@ import DrawerLayout from 'react-native-drawer-layout-polyfill';
import addNavigationHelpers from '../../addNavigationHelpers';
import DrawerSidebar from './DrawerSidebar';
import getChildEventSubscriber from '../../getChildEventSubscriber';
import NavigationActions from '../../NavigationActions';
/**
* Component that renders the drawer.
@@ -12,14 +12,12 @@ import getChildEventSubscriber from '../../getChildEventSubscriber';
export default class DrawerView extends React.PureComponent {
state = {
drawerWidth:
typeof this.props.drawerWidth === 'function'
? this.props.drawerWidth()
: this.props.drawerWidth,
typeof this.props.navigationConfig.drawerWidth === 'function'
? this.props.navigationConfig.drawerWidth()
: this.props.navigationConfig.drawerWidth,
};
componentWillMount() {
this._updateScreenNavigation(this.props.navigation);
Dimensions.addEventListener('change', this._updateWidth);
}
@@ -28,107 +26,59 @@ export default class DrawerView extends React.PureComponent {
}
componentWillReceiveProps(nextProps) {
if (
this.props.navigation.state.index !== nextProps.navigation.state.index
) {
const {
drawerOpenRoute,
drawerCloseRoute,
drawerToggleRoute,
} = this.props;
const { routes, index } = nextProps.navigation.state;
if (routes[index].routeName === drawerOpenRoute) {
this._drawer.openDrawer();
} else if (routes[index].routeName === drawerToggleRoute) {
if (this.props.navigation.state.index === 0) {
this.props.navigation.navigate(drawerOpenRoute);
} else {
this.props.navigation.navigate(drawerCloseRoute);
}
} else {
this._drawer.closeDrawer();
}
const { isDrawerOpen } = nextProps.navigation.state;
const wasDrawerOpen = this.props.navigation.state.isDrawerOpen;
if (isDrawerOpen && !wasDrawerOpen) {
this._drawer.openDrawer();
} else if (wasDrawerOpen && !isDrawerOpen) {
this._drawer.closeDrawer();
}
this._updateScreenNavigation(nextProps.navigation);
}
_handleDrawerOpen = () => {
const { navigation, drawerOpenRoute } = this.props;
const { routes, index } = navigation.state;
if (routes[index].routeName !== drawerOpenRoute) {
this.props.navigation.navigate(drawerOpenRoute);
}
const { navigation } = this.props;
navigation.dispatch({ type: NavigationActions.OPEN_DRAWER });
};
_handleDrawerClose = () => {
const { navigation, drawerCloseRoute } = this.props;
const { routes, index } = navigation.state;
if (routes[index].routeName !== drawerCloseRoute) {
this.props.navigation.navigate(drawerCloseRoute);
}
};
_updateScreenNavigation = navigation => {
const { drawerCloseRoute } = this.props;
const navigationState = navigation.state.routes.find(
route => route.routeName === drawerCloseRoute
);
if (
this._screenNavigationProp &&
this._screenNavigationProp.state === navigationState
) {
return;
}
this._screenNavigationProp = addNavigationHelpers({
dispatch: navigation.dispatch,
state: navigationState,
addListener: getChildEventSubscriber(
navigation.addListener,
navigationState.key
),
});
const { navigation } = this.props;
navigation.dispatch({ type: NavigationActions.CLOSE_DRAWER });
};
_updateWidth = () => {
const drawerWidth =
typeof this.props.drawerWidth === 'function'
? this.props.drawerWidth()
: this.props.drawerWidth;
typeof this.props.navigationConfig.drawerWidth === 'function'
? this.props.navigationConfig.drawerWidth()
: this.props.navigationConfig.drawerWidth;
if (this.state.drawerWidth !== drawerWidth) {
this.setState({ drawerWidth });
}
};
_getNavigationState = navigation => {
const { drawerCloseRoute } = this.props;
const navigationState = navigation.state.routes.find(
route => route.routeName === drawerCloseRoute
_renderNavigationView = () => {
return (
<DrawerSidebar
screenProps={this.props.screenProps}
navigation={this.props.navigation}
descriptors={this.props.descriptors}
contentComponent={this.props.navigationConfig.contentComponent}
contentOptions={this.props.navigationConfig.contentOptions}
drawerPosition={this.props.navigationConfig.drawerPosition}
style={this.props.navigationConfig.style}
{...this.props.navigationConfig}
/>
);
return navigationState;
};
_renderNavigationView = () => (
<DrawerSidebar
screenProps={this.props.screenProps}
navigation={this._screenNavigationProp}
router={this.props.router}
contentComponent={this.props.contentComponent}
contentOptions={this.props.contentOptions}
drawerPosition={this.props.drawerPosition}
style={this.props.style}
/>
);
render() {
const DrawerScreen = this.props.router.getComponentForRouteName(
this.props.drawerCloseRoute
);
const { state } = this.props.navigation;
const activeKey = state.routes[state.index].key;
const descriptor = this.props.descriptors[activeKey];
const config = this.props.router.getScreenOptions(
this._screenNavigationProp,
this.props.screenProps
);
const DrawerScreen = descriptor.getComponent();
const { drawerLockMode } = descriptor.options;
return (
<DrawerLayout
@@ -137,23 +87,25 @@ export default class DrawerView extends React.PureComponent {
}}
drawerLockMode={
(this.props.screenProps && this.props.screenProps.drawerLockMode) ||
(config && config.drawerLockMode)
this.props.navigationConfig.drawerLockMode
}
drawerBackgroundColor={
this.props.navigationConfig.drawerBackgroundColor
}
drawerBackgroundColor={this.props.drawerBackgroundColor}
drawerWidth={this.state.drawerWidth}
onDrawerOpen={this._handleDrawerOpen}
onDrawerClose={this._handleDrawerClose}
useNativeAnimations={this.props.useNativeAnimations}
useNativeAnimations={this.props.navigationConfig.useNativeAnimations}
renderNavigationView={this._renderNavigationView}
drawerPosition={
this.props.drawerPosition === 'right'
this.props.navigationConfig.drawerPosition === 'right'
? DrawerLayout.positions.Right
: DrawerLayout.positions.Left
}
>
<DrawerScreen
screenProps={this.props.screenProps}
navigation={this._screenNavigationProp}
navigation={descriptor.navigation}
/>
</DrawerLayout>
);

View File

@@ -3,25 +3,37 @@ import React from 'react';
import {
Animated,
Dimensions,
Image,
Platform,
StyleSheet,
View,
ViewPropTypes,
} from 'react-native';
import { MaskedViewIOS } from '../../PlatformHelpers';
import SafeAreaView from 'react-native-safe-area-view';
import HeaderTitle from './HeaderTitle';
import HeaderBackButton from './HeaderBackButton';
import ModularHeaderBackButton from './ModularHeaderBackButton';
import HeaderStyleInterpolator from './HeaderStyleInterpolator';
import SafeAreaView from '../SafeAreaView';
import withOrientation from '../withOrientation';
const APPBAR_HEIGHT = Platform.OS === 'ios' ? 44 : 56;
const STATUSBAR_HEIGHT = Platform.OS === 'ios' ? 20 : 0;
const TITLE_OFFSET = Platform.OS === 'ios' ? 70 : 56;
const getAppBarHeight = isLandscape => {
return Platform.OS === 'ios'
? isLandscape && !Platform.isPad ? 32 : 44
: 56;
};
class Header extends React.PureComponent {
static defaultProps = {
leftInterpolator: HeaderStyleInterpolator.forLeft,
leftButtonInterpolator: HeaderStyleInterpolator.forLeftButton,
leftLabelInterpolator: HeaderStyleInterpolator.forLeftLabel,
titleFromLeftInterpolator: HeaderStyleInterpolator.forCenterFromLeft,
titleInterpolator: HeaderStyleInterpolator.forCenter,
rightInterpolator: HeaderStyleInterpolator.forRight,
};
@@ -35,11 +47,11 @@ class Header extends React.PureComponent {
};
_getHeaderTitleString(scene) {
const sceneOptions = this.props.getScreenDetails(scene).options;
if (typeof sceneOptions.headerTitle === 'string') {
return sceneOptions.headerTitle;
const options = scene.descriptor.options;
if (typeof options.headerTitle === 'string') {
return options.headerTitle;
}
return sceneOptions.title;
return options.title;
}
_getLastScene(scene) {
@@ -51,7 +63,7 @@ class Header extends React.PureComponent {
if (!lastScene) {
return null;
}
const { headerBackTitle } = this.props.getScreenDetails(lastScene).options;
const { headerBackTitle } = lastScene.descriptor.options;
if (headerBackTitle || headerBackTitle === null) {
return headerBackTitle;
}
@@ -63,27 +75,20 @@ class Header extends React.PureComponent {
if (!lastScene) {
return null;
}
return this.props.getScreenDetails(lastScene).options
.headerTruncatedBackTitle;
return lastScene.descriptor.options.headerTruncatedBackTitle;
}
_navigateBack = () => {
requestAnimationFrame(() => {
this.props.navigation.goBack(this.props.scene.route.key);
});
};
_renderTitleComponent = props => {
const details = this.props.getScreenDetails(props.scene);
const headerTitle = details.options.headerTitle;
const { options } = props.scene.descriptor;
const headerTitle = options.headerTitle;
if (React.isValidElement(headerTitle)) {
return headerTitle;
}
const titleString = this._getHeaderTitleString(props.scene);
const titleStyle = details.options.headerTitleStyle;
const color = details.options.headerTintColor;
const allowFontScaling = details.options.headerTitleAllowFontScaling;
const titleStyle = options.headerTitleStyle;
const color = options.headerTintColor;
const allowFontScaling = options.headerTitleAllowFontScaling;
// On iOS, width of left/right components depends on the calculated
// size of the title.
@@ -115,16 +120,18 @@ class Header extends React.PureComponent {
};
_renderLeftComponent = props => {
const { options } = this.props.getScreenDetails(props.scene);
const { options } = props.scene.descriptor;
if (
React.isValidElement(options.headerLeft) ||
options.headerLeft === null
) {
return options.headerLeft;
}
if (props.scene.index === 0) {
return null;
return;
}
const backButtonTitle = this._getBackButtonTitleString(props.scene);
const truncatedBackButtonTitle = this._getTruncatedBackButtonTitle(
props.scene
@@ -133,9 +140,45 @@ class Header extends React.PureComponent {
? (this.props.layout.initWidth - this.state.widths[props.scene.key]) / 2
: undefined;
const RenderedLeftComponent = options.headerLeft || HeaderBackButton;
const goBack = () => {
// Go back on next tick because button ripple effect needs to happen on Android
requestAnimationFrame(() => {
this.props.navigation.goBack(props.scene.descriptor.key);
});
};
return (
<RenderedLeftComponent
onPress={goBack}
pressColorAndroid={options.headerPressColorAndroid}
tintColor={options.headerTintColor}
buttonImage={options.headerBackImage}
title={backButtonTitle}
truncatedTitle={truncatedBackButtonTitle}
titleStyle={options.headerBackTitleStyle}
width={width}
/>
);
};
_renderModularLeftComponent = (
props,
ButtonContainerComponent,
LabelContainerComponent
) => {
const { options } = props.scene.descriptor;
const backButtonTitle = this._getBackButtonTitleString(props.scene);
const truncatedBackButtonTitle = this._getTruncatedBackButtonTitle(
props.scene
);
const width = this.state.widths[props.scene.key]
? (this.props.layout.initWidth - this.state.widths[props.scene.key]) / 2
: undefined;
return (
<ModularHeaderBackButton
onPress={this._navigateBack}
ButtonContainerComponent={ButtonContainerComponent}
LabelContainerComponent={LabelContainerComponent}
pressColorAndroid={options.headerPressColorAndroid}
tintColor={options.headerTintColor}
buttonImage={options.headerBackImage}
@@ -148,22 +191,43 @@ class Header extends React.PureComponent {
};
_renderRightComponent = props => {
const details = this.props.getScreenDetails(props.scene);
const { headerRight } = details.options;
const { headerRight } = props.scene.descriptor.options;
return headerRight || null;
};
_renderLeft(props) {
return this._renderSubView(
props,
'left',
this._renderLeftComponent,
this.props.leftInterpolator
);
const { options } = props.scene.descriptor;
const { transitionPreset } = this.props;
// On Android, or if we have a custom header left, or if we have a custom back image, we
// do not use the modular header (which is the one that imitates UINavigationController)
if (
transitionPreset !== 'uikit' ||
options.headerBackImage ||
options.headerLeft ||
options.headerLeft === null
) {
return this._renderSubView(
props,
'left',
this._renderLeftComponent,
this.props.leftInterpolator
);
} else {
return this._renderModularSubView(
props,
'left',
this._renderModularLeftComponent,
this.props.leftLabelInterpolator,
this.props.leftButtonInterpolator
);
}
}
_renderTitle(props, options) {
const style = {};
const { transitionPreset } = this.props;
if (Platform.OS === 'android') {
if (!options.hasLeftComponent) {
@@ -185,7 +249,9 @@ class Header extends React.PureComponent {
{ ...props, style },
'title',
this._renderTitleComponent,
this.props.titleInterpolator
transitionPreset === 'uikit'
? this.props.titleFromLeftInterpolator
: this.props.titleInterpolator
);
}
@@ -198,6 +264,64 @@ class Header extends React.PureComponent {
);
}
_renderModularSubView(
props,
name,
renderer,
labelStyleInterpolator,
buttonStyleInterpolator
) {
const { scene } = props;
const { index, isStale, key } = scene;
// Never render a modular back button on the first screen in a stack.
if (index === 0) {
return;
}
const offset = this.props.navigation.state.index - index;
if (Math.abs(offset) > 2) {
// Scene is far away from the active scene. Hides it to avoid unnecessary
// rendering.
return null;
}
const ButtonContainer = ({ children }) => (
<Animated.View
style={[buttonStyleInterpolator({ ...this.props, ...props })]}
>
{children}
</Animated.View>
);
const LabelContainer = ({ children }) => (
<Animated.View
style={[labelStyleInterpolator({ ...this.props, ...props })]}
>
{children}
</Animated.View>
);
const subView = renderer(props, ButtonContainer, LabelContainer);
if (subView === null) {
return subView;
}
const pointerEvents = offset !== 0 || isStale ? 'none' : 'box-none';
return (
<View
key={`${name}_${key}`}
pointerEvents={pointerEvents}
style={[styles.item, styles[name], props.style]}
>
{subView}
</View>
);
}
_renderSubView(props, name, renderer, styleInterpolator) {
const { scene } = props;
const { index, isStale, key } = scene;
@@ -246,23 +370,59 @@ class Header extends React.PureComponent {
hasRightComponent: !!right,
});
return (
<View
style={[StyleSheet.absoluteFill, styles.header]}
key={`scene_${props.scene.key}`}
>
{title}
{left}
{right}
</View>
);
const { isLandscape, transitionPreset } = this.props;
const { options } = props.scene.descriptor;
const wrapperProps = {
style: styles.header,
key: `scene_${props.scene.key}`,
};
if (
options.headerLeft ||
options.headerBackImage ||
Platform.OS !== 'ios' ||
transitionPreset !== 'uikit'
) {
return (
<View {...wrapperProps}>
{title}
{left}
{right}
</View>
);
} else {
return (
<MaskedViewIOS
{...wrapperProps}
maskElement={
<View style={styles.iconMaskContainer}>
<Image
source={require('../assets/back-icon-mask.png')}
style={styles.iconMask}
/>
<View style={styles.iconMaskFillerRect} />
</View>
}
>
{title}
{left}
{right}
</MaskedViewIOS>
);
}
}
render() {
let appBar;
const { mode, scene, isLandscape } = this.props;
if (this.props.mode === 'float') {
const scenesProps = this.props.scenes.map(scene => ({
if (mode === 'float') {
const scenesByIndex = {};
this.props.scenes.forEach(scene => {
scenesByIndex[scene.index] = scene;
});
const scenesProps = Object.values(scenesByIndex).map(scene => ({
position: this.props.position,
progress: this.props.progress,
scene,
@@ -276,38 +436,59 @@ class Header extends React.PureComponent {
});
}
// eslint-disable-next-line no-unused-vars
const {
scenes,
scene,
position,
screenProps,
progress,
isLandscape,
...rest
} = this.props;
const { options } = scene.descriptor;
const { headerStyle = {} } = options;
const headerStyleObj = StyleSheet.flatten(headerStyle);
const appBarHeight = getAppBarHeight(isLandscape);
const { options } = this.props.getScreenDetails(scene);
const { headerStyle } = options;
const appBarHeight =
Platform.OS === 'ios' ? (isLandscape && !Platform.isPad ? 32 : 44) : 56;
const {
alignItems,
justifyContent,
flex,
flexDirection,
flexGrow,
flexShrink,
flexBasis,
flexWrap,
...safeHeaderStyle
} = headerStyleObj;
if (__DEV__) {
warnIfHeaderStyleDefined(alignItems, 'alignItems');
warnIfHeaderStyleDefined(justifyContent, 'justifyContent');
warnIfHeaderStyleDefined(flex, 'flex');
warnIfHeaderStyleDefined(flexDirection, 'flexDirection');
warnIfHeaderStyleDefined(flexGrow, 'flexGrow');
warnIfHeaderStyleDefined(flexShrink, 'flexShrink');
warnIfHeaderStyleDefined(flexBasis, 'flexBasis');
warnIfHeaderStyleDefined(flexWrap, 'flexWrap');
}
// TODO: warn if any unsafe styles are provided
const containerStyles = [
styles.container,
{
height: appBarHeight,
},
headerStyle,
options.headerTransparent
? styles.transparentContainer
: styles.container,
{ height: appBarHeight },
safeHeaderStyle,
];
const { headerForceInset } = options;
const forceInset = headerForceInset || { top: 'always', bottom: 'never' };
return (
<Animated.View {...rest}>
<SafeAreaView
style={containerStyles}
forceInset={{ top: 'always', bottom: 'never' }}
>
<View style={styles.appBar}>{appBar}</View>
</SafeAreaView>
</Animated.View>
<SafeAreaView forceInset={forceInset} style={containerStyles}>
<View style={StyleSheet.absoluteFill}>{options.headerBackground}</View>
<View style={{ flex: 1 }}>{appBar}</View>
</SafeAreaView>
);
}
}
function warnIfHeaderStyleDefined(value, styleProp) {
if (value !== undefined) {
console.warn(
`${styleProp} was given a value of ${value}, this has no effect on headerStyle.`
);
}
}
@@ -316,7 +497,7 @@ let platformContainerStyles;
if (Platform.OS === 'ios') {
platformContainerStyles = {
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: 'rgba(0, 0, 0, .3)',
borderBottomColor: '#A7A7AA',
};
} else {
platformContainerStyles = {
@@ -335,36 +516,64 @@ const styles = StyleSheet.create({
backgroundColor: Platform.OS === 'ios' ? '#F7F7F7' : '#FFF',
...platformContainerStyles,
},
appBar: {
flex: 1,
transparentContainer: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
...platformContainerStyles,
},
header: {
...StyleSheet.absoluteFillObject,
flexDirection: 'row',
},
item: {
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'transparent',
},
iconMaskContainer: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
},
iconMaskFillerRect: {
flex: 1,
backgroundColor: '#d8d8d8',
marginLeft: -3,
},
iconMask: {
// These are mostly the same as the icon in ModularHeaderBackButton
height: 21,
width: 12,
marginLeft: 9,
marginTop: -0.5, // resizes down to 20.5
alignSelf: 'center',
resizeMode: 'contain',
},
title: {
bottom: 0,
top: 0,
left: TITLE_OFFSET,
right: TITLE_OFFSET,
top: 0,
position: 'absolute',
alignItems: Platform.OS === 'ios' ? 'center' : 'flex-start',
alignItems: 'center',
flexDirection: 'row',
justifyContent: Platform.OS === 'ios' ? 'center' : 'flex-start',
},
left: {
left: 0,
bottom: 0,
top: 0,
position: 'absolute',
alignItems: 'center',
flexDirection: 'row',
},
right: {
right: 0,
bottom: 0,
top: 0,
position: 'absolute',
flexDirection: 'row',
alignItems: 'center',
},
});

594
src/views/Header/Header2.js Normal file
View File

@@ -0,0 +1,594 @@
import React from 'react';
import {
Animated,
Dimensions,
Image,
Platform,
StyleSheet,
View,
ViewPropTypes,
} from 'react-native';
import { MaskedViewIOS } from '../../PlatformHelpers';
import SafeAreaView from 'react-native-safe-area-view';
import HeaderTitle from './HeaderTitle';
import HeaderBackButton from './HeaderBackButton';
import ModularHeaderBackButton from './ModularHeaderBackButton';
import HeaderStyleInterpolator from './HeaderStyleInterpolator2';
import withOrientation from '../withOrientation';
import { last } from 'rxjs/operators';
const APPBAR_HEIGHT = Platform.OS === 'ios' ? 44 : 56;
const STATUSBAR_HEIGHT = Platform.OS === 'ios' ? 20 : 0;
const TITLE_OFFSET = Platform.OS === 'ios' ? 70 : 56;
const getAppBarHeight = isLandscape => {
return Platform.OS === 'ios'
? isLandscape && !Platform.isPad ? 32 : 44
: 56;
};
class Header extends React.PureComponent {
static defaultProps = {
leftInterpolator: HeaderStyleInterpolator.forLeft,
leftButtonInterpolator: HeaderStyleInterpolator.forLeftButton,
leftLabelInterpolator: HeaderStyleInterpolator.forLeftLabel,
titleFromLeftInterpolator: HeaderStyleInterpolator.forCenterFromLeft,
titleInterpolator: HeaderStyleInterpolator.forCenter,
rightInterpolator: HeaderStyleInterpolator.forRight,
};
static get HEIGHT() {
return APPBAR_HEIGHT + STATUSBAR_HEIGHT;
}
state = {
widths: {},
};
_getHeaderTitleString({ options }) {
if (typeof options.headerTitle === 'string') {
return options.headerTitle;
}
return options.title;
}
_getLastSceneDescriptor(descriptor) {
const { state } = this.props.navigation;
const index = state.routes.findIndex(r => r.key === descriptor.key);
if (index < 1) {
return null;
}
const lastKey = state.routes[index - 1].key;
return this.props.descriptors[lastKey];
}
_getBackButtonTitleString(descriptor) {
const lastSceneDescriptor = this._getLastSceneDescriptor(descriptor);
if (!lastSceneDescriptor) {
return null;
}
const { headerBackTitle } = lastSceneDescriptor.options;
if (headerBackTitle || headerBackTitle === null) {
return headerBackTitle;
}
return this._getHeaderTitleString(lastSceneDescriptor);
}
_getTruncatedBackButtonTitle(descriptor) {
const lastSceneDescriptor = this._getLastSceneDescriptor(descriptor);
if (!lastSceneDescriptor) {
return null;
}
return lastSceneDescriptor.options.headerTruncatedBackTitle;
}
_renderTitleComponent = props => {
const { options } = props.descriptor;
const headerTitle = options.headerTitle;
if (React.isValidElement(headerTitle)) {
return headerTitle;
}
const titleString = this._getHeaderTitleString(props.descriptor);
const titleStyle = options.headerTitleStyle;
const color = options.headerTintColor;
const allowFontScaling = options.headerTitleAllowFontScaling;
// On iOS, width of left/right components depends on the calculated
// size of the title.
const onLayoutIOS =
Platform.OS === 'ios'
? e => {
this.setState({
widths: {
...this.state.widths,
[props.descriptor.key]: e.nativeEvent.layout.width,
},
});
}
: undefined;
const RenderedHeaderTitle =
headerTitle && typeof headerTitle !== 'string'
? headerTitle
: HeaderTitle;
return (
<RenderedHeaderTitle
onLayout={onLayoutIOS}
allowFontScaling={allowFontScaling == null ? true : allowFontScaling}
style={[color ? { color } : null, titleStyle]}
>
{titleString}
</RenderedHeaderTitle>
);
};
_renderLeftComponent = props => {
const { options } = props.descriptor;
if (
React.isValidElement(options.headerLeft) ||
options.headerLeft === null
) {
return options.headerLeft;
}
if (props.index === 0) {
return;
}
const backButtonTitle = this._getBackButtonTitleString(props.descriptor);
const truncatedBackButtonTitle = this._getTruncatedBackButtonTitle(
props.descriptor
);
const width = this.state.widths[props.descriptor.key]
? (this.props.layout.initWidth -
this.state.widths[props.descriptor.key]) /
2
: undefined;
const RenderedLeftComponent = options.headerLeft || HeaderBackButton;
const goBack = () => {
// Go back on next tick because button ripple effect needs to happen on Android
requestAnimationFrame(() => {
this.props.navigation.goBack(props.descriptor.key);
});
};
return (
<RenderedLeftComponent
onPress={goBack}
pressColorAndroid={options.headerPressColorAndroid}
tintColor={options.headerTintColor}
buttonImage={options.headerBackImage}
title={backButtonTitle}
truncatedTitle={truncatedBackButtonTitle}
titleStyle={options.headerBackTitleStyle}
width={width}
/>
);
};
_renderModularLeftComponent = (
props,
ButtonContainerComponent,
LabelContainerComponent
) => {
const { options } = props.descriptor;
const backButtonTitle = this._getBackButtonTitleString(props.descriptor);
const truncatedBackButtonTitle = this._getTruncatedBackButtonTitle(
props.descriptor
);
const width = this.state.widths[props.descriptor.key]
? (this.props.layout.initWidth -
this.state.widths[props.descriptor.key]) /
2
: undefined;
return (
<ModularHeaderBackButton
onPress={this._navigateBack}
ButtonContainerComponent={ButtonContainerComponent}
LabelContainerComponent={LabelContainerComponent}
pressColorAndroid={options.headerPressColorAndroid}
tintColor={options.headerTintColor}
buttonImage={options.headerBackImage}
title={backButtonTitle}
truncatedTitle={truncatedBackButtonTitle}
titleStyle={options.headerBackTitleStyle}
width={width}
/>
);
};
_renderRightComponent = props => {
const { headerRight } = props.descriptor.options;
return headerRight || null;
};
_renderLeft(props) {
const { options } = props.descriptor;
const { transitionPreset } = this.props;
// On Android, or if we have a custom header left, or if we have a custom back image, we
// do not use the modular header (which is the one that imitates UINavigationController)
if (
transitionPreset !== 'uikit' ||
options.headerBackImage ||
options.headerLeft ||
options.headerLeft === null
) {
return this._renderSubView(
props,
'left',
this._renderLeftComponent,
this.props.leftInterpolator
);
} else {
return this._renderModularSubView(
props,
'left',
this._renderModularLeftComponent,
this.props.leftLabelInterpolator,
this.props.leftButtonInterpolator
);
}
}
_renderTitle(props, options) {
const style = {};
const { transitionPreset } = this.props;
if (Platform.OS === 'android') {
if (!options.hasLeftComponent) {
style.left = 0;
}
if (!options.hasRightComponent) {
style.right = 0;
}
} else if (
Platform.OS === 'ios' &&
!options.hasLeftComponent &&
!options.hasRightComponent
) {
style.left = 0;
style.right = 0;
}
return this._renderSubView(
{ ...props, style },
'title',
this._renderTitleComponent,
transitionPreset === 'uikit'
? this.props.titleFromLeftInterpolator
: this.props.titleInterpolator
);
}
_renderRight(props) {
return this._renderSubView(
props,
'right',
this._renderRightComponent,
this.props.rightInterpolator
);
}
_renderModularSubView(
props,
name,
renderer,
labelStyleInterpolator,
buttonStyleInterpolator
) {
const { descriptor, index, navigation } = props;
const { key } = descriptor;
// Never render a modular back button on the first screen in a stack.
if (index === 0) {
return;
}
const offset = navigation.state.index - index;
if (Math.abs(offset) > 2) {
// Scene is far away from the active scene. Hides it to avoid unnecessary
// rendering.
return null;
}
const ButtonContainer = ({ children }) => (
<Animated.View style={[buttonStyleInterpolator(props)]}>
{children}
</Animated.View>
);
const LabelContainer = ({ children }) => (
<Animated.View style={[labelStyleInterpolator(props)]}>
{children}
</Animated.View>
);
const subView = renderer(props, ButtonContainer, LabelContainer);
if (subView === null) {
return subView;
}
const isTransitioning = !!navigation.state.transitioningFromKey;
const pointerEvents = offset !== 0 || isTransitioning ? 'none' : 'box-none';
return (
<View
key={`${name}_${key}`}
pointerEvents={pointerEvents}
style={[styles.item, styles[name], props.style]}
>
{subView}
</View>
);
}
_renderSubView(props, name, renderer, styleInterpolator) {
const { descriptor, index, navigation } = props;
const { key } = descriptor;
const offset = navigation.state.index - index;
if (Math.abs(offset) > 2) {
// Scene is far away from the active scene. Hides it to avoid unnecessary
// rendering.
return null;
}
const subView = renderer(props);
if (subView == null) {
return null;
}
const isTransitioning = !!navigation.state.transitioningFromKey;
const pointerEvents = offset !== 0 || isTransitioning ? 'none' : 'box-none';
return (
<Animated.View
pointerEvents={pointerEvents}
key={`${name}_${key}`}
style={[
styles.item,
styles[name],
props.style,
styleInterpolator({
// todo: determine if we really need to splat all this.props
...this.props,
...props,
}),
]}
>
{subView}
</Animated.View>
);
}
_renderHeader(props) {
const left = this._renderLeft(props);
const right = this._renderRight(props);
const title = this._renderTitle(props, {
hasLeftComponent: !!left,
hasRightComponent: !!right,
});
const { isLandscape, transitionPreset } = this.props;
const { options } = props.descriptor;
const wrapperProps = {
style: styles.header,
key: `header_${props.descriptor.key}`,
};
if (
options.headerLeft ||
options.headerBackImage ||
Platform.OS !== 'ios' ||
transitionPreset !== 'uikit'
) {
return (
<View {...wrapperProps}>
{title}
{left}
{right}
</View>
);
} else {
return (
<MaskedViewIOS
{...wrapperProps}
maskElement={
<View style={styles.iconMaskContainer}>
<Image
source={require('../assets/back-icon-mask.png')}
style={styles.iconMask}
/>
<View style={styles.iconMaskFillerRect} />
</View>
}
>
{title}
{left}
{right}
</MaskedViewIOS>
);
}
}
render() {
let appBar;
const {
mode,
isLandscape,
navigation,
descriptors,
descriptor,
transition,
} = this.props;
const { index } = navigation.state;
if (mode === 'float') {
const scenesDescriptorsByIndex = [];
const { state } = navigation;
state.routes.forEach((route, routeIndex) => {
scenesDescriptorsByIndex[routeIndex] = descriptors[route.key];
});
const scenesProps = scenesDescriptorsByIndex.map(
(descriptor, descriptorIndex) => ({
...this.props,
descriptor,
index: descriptorIndex,
})
);
appBar = scenesProps.map(this._renderHeader, this);
} else {
appBar = this._renderHeader({ ...this.props, index });
}
const { options } = descriptor;
const { headerStyle = {} } = options;
const headerStyleObj = StyleSheet.flatten(headerStyle);
const appBarHeight = getAppBarHeight(isLandscape);
const {
alignItems,
justifyContent,
flex,
flexDirection,
flexGrow,
flexShrink,
flexBasis,
flexWrap,
...safeHeaderStyle
} = headerStyleObj;
if (__DEV__) {
warnIfHeaderStyleDefined(alignItems, 'alignItems');
warnIfHeaderStyleDefined(justifyContent, 'justifyContent');
warnIfHeaderStyleDefined(flex, 'flex');
warnIfHeaderStyleDefined(flexDirection, 'flexDirection');
warnIfHeaderStyleDefined(flexGrow, 'flexGrow');
warnIfHeaderStyleDefined(flexShrink, 'flexShrink');
warnIfHeaderStyleDefined(flexBasis, 'flexBasis');
warnIfHeaderStyleDefined(flexWrap, 'flexWrap');
}
// TODO: warn if any unsafe styles are provided
const containerStyles = [
options.headerTransparent
? styles.transparentContainer
: styles.container,
{ height: appBarHeight },
safeHeaderStyle,
];
const { headerForceInset } = options;
const forceInset = headerForceInset || { top: 'always', bottom: 'never' };
return (
<SafeAreaView forceInset={forceInset} style={containerStyles}>
<View style={StyleSheet.absoluteFill}>{options.headerBackground}</View>
<View style={{ flex: 1 }}>{appBar}</View>
</SafeAreaView>
);
}
}
function warnIfHeaderStyleDefined(value, styleProp) {
if (value !== undefined) {
console.warn(
`${styleProp} was given a value of ${value}, this has no effect on headerStyle.`
);
}
}
let platformContainerStyles;
if (Platform.OS === 'ios') {
platformContainerStyles = {
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#A7A7AA',
};
} else {
platformContainerStyles = {
shadowColor: 'black',
shadowOpacity: 0.1,
shadowRadius: StyleSheet.hairlineWidth,
shadowOffset: {
height: StyleSheet.hairlineWidth,
},
elevation: 4,
};
}
const styles = StyleSheet.create({
container: {
backgroundColor: Platform.OS === 'ios' ? '#F7F7F7' : '#FFF',
...platformContainerStyles,
},
transparentContainer: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
...platformContainerStyles,
},
header: {
...StyleSheet.absoluteFillObject,
flexDirection: 'row',
},
item: {
backgroundColor: 'transparent',
},
iconMaskContainer: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
},
iconMaskFillerRect: {
flex: 1,
backgroundColor: '#d8d8d8',
marginLeft: -3,
},
iconMask: {
// These are mostly the same as the icon in ModularHeaderBackButton
height: 21,
width: 12,
marginLeft: 9,
marginTop: -0.5, // resizes down to 20.5
alignSelf: 'center',
resizeMode: 'contain',
},
title: {
bottom: 0,
top: 0,
left: TITLE_OFFSET,
right: TITLE_OFFSET,
position: 'absolute',
alignItems: 'center',
flexDirection: 'row',
justifyContent: Platform.OS === 'ios' ? 'center' : 'flex-start',
},
left: {
left: 0,
bottom: 0,
top: 0,
position: 'absolute',
alignItems: 'center',
flexDirection: 'row',
},
right: {
right: 0,
bottom: 0,
top: 0,
position: 'absolute',
flexDirection: 'row',
alignItems: 'center',
},
});
export default withOrientation(Header);

View File

@@ -107,7 +107,7 @@ const styles = StyleSheet.create({
? {
height: 21,
width: 13,
marginLeft: 10,
marginLeft: 9,
marginRight: 22,
marginVertical: 12,
resizeMode: 'contain',
@@ -123,7 +123,7 @@ const styles = StyleSheet.create({
iconWithTitle:
Platform.OS === 'ios'
? {
marginRight: 5,
marginRight: 6,
}
: {},
});

View File

@@ -1,7 +1,11 @@
import { I18nManager } from 'react-native';
import { Dimensions, I18nManager } from 'react-native';
import getSceneIndicesForInterpolationInputRange from '../../utils/getSceneIndicesForInterpolationInputRange';
const crossFadeInterpolation = (first, index, last) => ({
inputRange: [first, index - 0.9, index - 0.2, index, last],
outputRange: [0, 0, 0.3, 1, 0],
});
/**
* Utility that builds the style for the navigation header.
*
@@ -23,16 +27,7 @@ function forLeft(props) {
const index = scene.index;
return {
opacity: position.interpolate({
inputRange: [
first,
first + Math.abs(index - first) / 2,
index,
last - Math.abs(last - index) / 2,
last,
],
outputRange: [0, 0, 1, 0, 0],
}),
opacity: position.interpolate(crossFadeInterpolation(first, index, last)),
};
}
@@ -44,21 +39,9 @@ function forCenter(props) {
const { first, last } = interpolate;
const index = scene.index;
const inputRange = [first, index, last];
return {
opacity: position.interpolate({
inputRange,
outputRange: [0, 1, 0],
}),
transform: [
{
translateX: position.interpolate({
inputRange,
outputRange: I18nManager.isRTL ? [-200, 0, 200] : [200, 0, -200],
}),
},
],
opacity: position.interpolate(crossFadeInterpolation(first, index, last)),
};
}
@@ -70,16 +53,125 @@ function forRight(props) {
const { first, last } = interpolate;
const index = scene.index;
return {
opacity: position.interpolate(crossFadeInterpolation(first, index, last)),
};
}
/**
* iOS UINavigationController style interpolators
*/
function forLeftButton(props) {
const { position, scene, scenes } = props;
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
const { first, last } = interpolate;
const index = scene.index;
return {
opacity: position.interpolate({
inputRange: [first, index, last],
outputRange: [0, 1, 0],
inputRange: [
first,
first + Math.abs(index - first) / 2,
index,
last - Math.abs(last - index) / 2,
last,
],
outputRange: [0, 0.5, 1, 0.5, 0],
}),
};
}
/*
* NOTE: this offset calculation is a an approximation that gives us
* decent results in many cases, but it is ultimately a poor substitute
* for text measurement. See the comment on title for more information.
*
* - 70 is the width of the left button area.
* - 25 is the width of the left button icon (to account for label offset)
*/
const LEFT_LABEL_OFFSET = Dimensions.get('window').width / 2 - 70 - 25;
function forLeftLabel(props) {
const { position, scene, scenes } = props;
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
const { first, last } = interpolate;
const index = scene.index;
const offset = LEFT_LABEL_OFFSET;
return {
// For now we fade out the label before fading in the title, so the
// differences between the label and title position can be hopefully not so
// noticable to the user
opacity: position.interpolate({
inputRange: [first, index - 0.35, index, index + 0.5, last],
outputRange: [0, 0, 1, 0.5, 0],
}),
transform: [
{
translateX: position.interpolate({
inputRange: [first, index, last],
outputRange: I18nManager.isRTL
? [-offset, 0, offset]
: [offset, 0, -offset * 1.5],
}),
},
],
};
}
/*
* NOTE: this offset calculation is a an approximation that gives us
* decent results in many cases, but it is ultimately a poor substitute
* for text measurement. We want the back button label to transition
* smoothly into the title text and to do this we need to understand
* where the title is positioned within the title container (since it is
* centered).
*
* - 70 is the width of the left button area.
* - 25 is the width of the left button icon (to account for label offset)
*/
const TITLE_OFFSET_IOS = Dimensions.get('window').width / 2 - 70 + 25;
function forCenterFromLeft(props) {
const { position, scene } = props;
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
const { first, last } = interpolate;
const index = scene.index;
const inputRange = [first, index - 0.5, index, index + 0.5, last];
const offset = TITLE_OFFSET_IOS;
return {
opacity: position.interpolate({
inputRange: [first, index - 0.5, index, index + 0.7, last],
outputRange: [0, 0, 1, 0, 0],
}),
transform: [
{
translateX: position.interpolate({
inputRange: [first, index, last],
outputRange: I18nManager.isRTL
? [-offset, 0, offset]
: [offset, 0, -offset],
}),
},
],
};
}
export default {
forLeft,
forLeftButton,
forLeftLabel,
forCenterFromLeft,
forCenter,
forRight,
};

View File

@@ -0,0 +1,176 @@
import { Dimensions, I18nManager } from 'react-native';
const crossFadeInterpolation = (first, index, last) => ({
inputRange: [first, index - 0.9, index - 0.2, index, last],
outputRange: [0, 0, 0.3, 1, 0],
});
/**
* Utility that builds the style for the navigation header.
*
* +-------------+-------------+-------------+
* | | | |
* | Left | Title | Right |
* | Component | Component | Component |
* | | | |
* +-------------+-------------+-------------+
*/
function forLeft(props) {
const { position, descriptor, scenes } = props;
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
const { first, last } = interpolate;
const index = scene.index;
return {
opacity: position.interpolate(crossFadeInterpolation(first, index, last)),
};
}
function forCenter(props) {
const { position, scene } = props;
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
const { first, last } = interpolate;
const index = scene.index;
return {
opacity: position.interpolate(crossFadeInterpolation(first, index, last)),
};
}
function forRight(props) {
const { position, scene } = props;
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
const { first, last } = interpolate;
const index = scene.index;
return {
opacity: position.interpolate(crossFadeInterpolation(first, index, last)),
};
}
/**
* iOS UINavigationController style interpolators
*/
function forLeftButton(props) {
const { position, scene, scenes } = props;
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
const { first, last } = interpolate;
const index = scene.index;
return {
opacity: position.interpolate({
inputRange: [
first,
first + Math.abs(index - first) / 2,
index,
last - Math.abs(last - index) / 2,
last,
],
outputRange: [0, 0.5, 1, 0.5, 0],
}),
};
}
/*
* NOTE: this offset calculation is a an approximation that gives us
* decent results in many cases, but it is ultimately a poor substitute
* for text measurement. See the comment on title for more information.
*
* - 70 is the width of the left button area.
* - 25 is the width of the left button icon (to account for label offset)
*/
const LEFT_LABEL_OFFSET = Dimensions.get('window').width / 2 - 70 - 25;
function forLeftLabel(props) {
const { position, scene, scenes } = props;
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
const { first, last } = interpolate;
const index = scene.index;
const offset = LEFT_LABEL_OFFSET;
return {
// For now we fade out the label before fading in the title, so the
// differences between the label and title position can be hopefully not so
// noticable to the user
opacity: position.interpolate({
inputRange: [first, index - 0.35, index, index + 0.5, last],
outputRange: [0, 0, 1, 0.5, 0],
}),
transform: [
{
translateX: position.interpolate({
inputRange: [first, index, last],
outputRange: I18nManager.isRTL
? [-offset, 0, offset]
: [offset, 0, -offset * 1.5],
}),
},
],
};
}
/*
* NOTE: this offset calculation is a an approximation that gives us
* decent results in many cases, but it is ultimately a poor substitute
* for text measurement. We want the back button label to transition
* smoothly into the title text and to do this we need to understand
* where the title is positioned within the title container (since it is
* centered).
*
* - 70 is the width of the left button area.
* - 25 is the width of the left button icon (to account for label offset)
*/
const TITLE_OFFSET_IOS = Dimensions.get('window').width / 2 - 70 + 25;
function forCenterFromLeft(props) {
const { position, scene } = props;
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
const { first, last } = interpolate;
const index = scene.index;
const inputRange = [first, index - 0.5, index, index + 0.5, last];
const offset = TITLE_OFFSET_IOS;
return {
opacity: position.interpolate({
inputRange: [first, index - 0.5, index, index + 0.7, last],
outputRange: [0, 0, 1, 0, 0],
}),
transform: [
{
translateX: position.interpolate({
inputRange: [first, index, last],
outputRange: I18nManager.isRTL
? [-offset, 0, offset]
: [offset, 0, -offset],
}),
},
],
};
}
export default {
forLeft,
forLeftButton,
forLeftLabel,
forCenterFromLeft,
forCenter,
forRight,
};

View File

@@ -15,7 +15,7 @@ const HeaderTitle = ({ style, ...rest }) => (
const styles = StyleSheet.create({
title: {
fontSize: Platform.OS === 'ios' ? 17 : 20,
fontWeight: Platform.OS === 'ios' ? '600' : '500',
fontWeight: Platform.OS === 'ios' ? '700' : '500',
color: 'rgba(0, 0, 0, .9)',
textAlign: Platform.OS === 'ios' ? 'center' : 'left',
marginHorizontal: 16,

View File

@@ -0,0 +1,118 @@
import React from 'react';
import { I18nManager, Image, Text, View, StyleSheet } from 'react-native';
import TouchableItem from '../TouchableItem';
class ModularHeaderBackButton extends React.PureComponent {
static defaultProps = {
tintColor: '#037aff',
truncatedTitle: 'Back',
// eslint-disable-next-line global-require
buttonImage: require('../assets/back-icon.png'),
};
state = {};
_onTextLayout = e => {
if (this.state.initialTextWidth) {
return;
}
this.setState({
initialTextWidth: e.nativeEvent.layout.x + e.nativeEvent.layout.width,
});
};
render() {
const {
buttonImage,
onPress,
width,
title,
titleStyle,
tintColor,
truncatedTitle,
} = this.props;
const renderTruncated =
this.state.initialTextWidth && width
? this.state.initialTextWidth > width
: false;
let backButtonTitle = renderTruncated ? truncatedTitle : title;
// TODO: When we've sorted out measuring in the header, let's revisit this.
// This is clearly a bad workaround.
if (backButtonTitle && backButtonTitle.length > 8) {
backButtonTitle = truncatedTitle;
}
const { ButtonContainerComponent, LabelContainerComponent } = this.props;
return (
<TouchableItem
accessibilityComponentType="button"
accessibilityLabel={backButtonTitle}
accessibilityTraits="button"
testID="header-back"
delayPressIn={0}
onPress={onPress}
style={styles.container}
borderless
>
<View style={styles.container}>
<ButtonContainerComponent>
<Image
style={[
styles.icon,
!!title && styles.iconWithTitle,
!!tintColor && { tintColor },
]}
source={buttonImage}
/>
</ButtonContainerComponent>
{typeof backButtonTitle === 'string' && (
<LabelContainerComponent>
<Text
onLayout={this._onTextLayout}
style={[
styles.title,
!!tintColor && { color: tintColor },
titleStyle,
]}
numberOfLines={1}
>
{backButtonTitle}
</Text>
</LabelContainerComponent>
)}
</View>
</TouchableItem>
);
}
}
const styles = StyleSheet.create({
container: {
alignItems: 'center',
flexDirection: 'row',
backgroundColor: 'transparent',
},
title: {
fontSize: 17,
paddingRight: 10,
},
icon: {
height: 21,
width: 12,
marginLeft: 9,
marginRight: 22,
marginVertical: 12,
resizeMode: 'contain',
transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }],
},
iconWithTitle: {
marginRight: 3,
},
});
export default ModularHeaderBackButton;

View File

@@ -0,0 +1,112 @@
import React from 'react';
import { Platform, StyleSheet, View } from 'react-native';
import PropTypes from 'prop-types';
import SceneView from './SceneView';
const FAR_FAR_AWAY = 3000; // this should be big enough to move the whole view out of its container
export default class ResourceSavingSceneView extends React.PureComponent {
constructor(props) {
super();
const key = props.childNavigation.state.key;
const focusedIndex = props.navigation.state.index;
const focusedKey = props.navigation.state.routes[focusedIndex].key;
const isFocused = key === focusedKey;
this.state = {
awake: props.lazy ? isFocused : true,
visible: isFocused,
};
}
componentWillMount() {
this._actionListener = this.props.navigation.addListener(
'action',
this._onAction
);
}
componentWillUnmount() {
this._actionListener.remove();
}
render() {
const { awake, visible } = this.state;
const {
childNavigation,
navigation,
removeClippedSubviews,
lazy,
...rest
} = this.props;
return (
<View
style={styles.container}
collapsable={false}
removeClippedSubviews={
Platform.OS === 'android'
? removeClippedSubviews
: !visible && removeClippedSubviews
}
>
<View
style={
this._mustAlwaysBeVisible() || visible
? styles.innerAttached
: styles.innerDetached
}
>
{awake ? <SceneView {...rest} navigation={childNavigation} /> : null}
</View>
</View>
);
}
_mustAlwaysBeVisible = () => {
return this.props.animationEnabled || this.props.swipeEnabled;
};
_onAction = payload => {
// We do not care about transition complete events, they won't actually change the state
if (
payload.action.type == 'Navigation/COMPLETE_TRANSITION' ||
!payload.state
) {
return;
}
const { routes, index } = payload.state;
const key = this.props.childNavigation.state.key;
if (routes[index].key === key) {
if (!this.state.visible) {
let nextState = { visible: true };
if (!this.state.awake) {
nextState.awake = true;
}
this.setState(nextState);
}
} else {
if (this.state.visible) {
this.setState({ visible: false });
}
}
};
}
const styles = StyleSheet.create({
container: {
flex: 1,
overflow: 'hidden',
},
innerAttached: {
flex: 1,
},
innerDetached: {
flex: 1,
top: FAR_FAR_AWAY,
},
});

View File

@@ -1,316 +0,0 @@
import React, { Component } from 'react';
import {
DeviceInfo,
Dimensions,
InteractionManager,
NativeModules,
Platform,
StyleSheet,
Animated,
} from 'react-native';
import withOrientation from './withOrientation';
// See https://mydevice.io/devices/ for device dimensions
const X_WIDTH = 375;
const X_HEIGHT = 812;
const PAD_WIDTH = 768;
const PAD_HEIGHT = 1024;
const { height: D_HEIGHT, width: D_WIDTH } = Dimensions.get('window');
const { PlatformConstants = {} } = NativeModules;
const { minor = 0 } = PlatformConstants.reactNativeVersion || {};
const isIPhoneX = (() => {
if (Platform.OS === 'web') return false;
if (minor >= 50) {
return DeviceInfo.isIPhoneX_deprecated;
}
return (
Platform.OS === 'ios' &&
((D_HEIGHT === X_HEIGHT && D_WIDTH === X_WIDTH) ||
(D_HEIGHT === X_WIDTH && D_WIDTH === X_HEIGHT))
);
})();
const isIPad = (() => {
if (Platform.OS !== 'ios' || isIPhoneX) return false;
// if portrait and width is smaller than iPad width
if (D_HEIGHT > D_WIDTH && D_WIDTH < PAD_WIDTH) {
return false;
}
// if landscape and height is smaller that iPad height
if (D_WIDTH > D_HEIGHT && D_HEIGHT < PAD_WIDTH) {
return false;
}
return true;
})();
let _customStatusBarHeight = null;
const statusBarHeight = isLandscape => {
if (_customStatusBarHeight !== null) {
return _customStatusBarHeight;
}
/**
* This is a temporary workaround because we don't have a way to detect
* if the status bar is translucent or opaque. If opaque, we don't need to
* factor in the height here; if translucent (content renders under it) then
* we do.
*/
if (Platform.OS === 'android') {
if (global.Expo) {
return global.Expo.Constants.statusBarHeight;
} else {
return 0;
}
}
if (isIPhoneX) {
return isLandscape ? 0 : 44;
}
if (isIPad) {
return 20;
}
return isLandscape ? 0 : 20;
};
const doubleFromPercentString = percent => {
if (!percent.includes('%')) {
return 0;
}
const dbl = parseFloat(percent) / 100;
if (isNaN(dbl)) return 0;
return dbl;
};
class SafeView extends Component {
static setStatusBarHeight = height => {
_customStatusBarHeight = height;
};
state = {
touchesTop: true,
touchesBottom: true,
touchesLeft: true,
touchesRight: true,
orientation: null,
viewWidth: 0,
viewHeight: 0,
};
componentDidMount() {
InteractionManager.runAfterInteractions(() => {
this._onLayout();
});
}
componentWillReceiveProps() {
this._onLayout();
}
render() {
const { forceInset = false, isLandscape, children, style } = this.props;
const safeAreaStyle = this._getSafeAreaStyle();
return (
<Animated.View
ref={c => (this.view = c)}
onLayout={this._onLayout}
style={safeAreaStyle}
>
{this.props.children}
</Animated.View>
);
}
_onLayout = () => {
if (!this.view) return;
const { isLandscape } = this.props;
const { orientation } = this.state;
const newOrientation = isLandscape ? 'landscape' : 'portrait';
if (orientation && orientation === newOrientation) {
return;
}
const WIDTH = isLandscape ? X_HEIGHT : X_WIDTH;
const HEIGHT = isLandscape ? X_WIDTH : X_HEIGHT;
this.view._component.measureInWindow((winX, winY, winWidth, winHeight) => {
let realY = winY;
let realX = winX;
if (realY >= HEIGHT) {
realY = realY % HEIGHT;
} else if (realY < 0) {
realY = realY % HEIGHT + HEIGHT;
}
if (realX >= WIDTH) {
realX = realX % WIDTH;
} else if (realX < 0) {
realX = realX % WIDTH + WIDTH;
}
const touchesTop = realY === 0;
const touchesBottom = realY + winHeight >= HEIGHT;
const touchesLeft = realX === 0;
const touchesRight = realX + winWidth >= WIDTH;
this.setState({
touchesTop,
touchesBottom,
touchesLeft,
touchesRight,
orientation: newOrientation,
viewWidth: winWidth,
viewHeight: winHeight,
});
});
};
_getSafeAreaStyle = () => {
const { touchesTop, touchesBottom, touchesLeft, touchesRight } = this.state;
const { forceInset, isLandscape } = this.props;
const {
paddingTop,
paddingBottom,
paddingLeft,
paddingRight,
viewStyle,
} = this._getViewStyles();
const style = {
...viewStyle,
paddingTop: touchesTop ? this._getInset('top') : 0,
paddingBottom: touchesBottom ? this._getInset('bottom') : 0,
paddingLeft: touchesLeft ? this._getInset('left') : 0,
paddingRight: touchesRight ? this._getInset('right') : 0,
};
if (forceInset) {
Object.keys(forceInset).forEach(key => {
let inset = forceInset[key];
if (inset === 'always') {
inset = this._getInset(key);
}
if (inset === 'never') {
inset = 0;
}
switch (key) {
case 'horizontal': {
style.paddingLeft = inset;
style.paddingRight = inset;
break;
}
case 'vertical': {
style.paddingTop = inset;
style.paddingBottom = inset;
break;
}
case 'left':
case 'right':
case 'top':
case 'bottom': {
const padding = `padding${key[0].toUpperCase()}${key.slice(1)}`;
style[padding] = inset;
break;
}
}
});
}
// new height/width should only include padding from insets
// height/width should not be affected by padding from style obj
if (style.height && typeof style.height === 'number') {
style.height += style.paddingTop + style.paddingBottom;
}
if (style.width && typeof style.width === 'number') {
style.width += style.paddingLeft + style.paddingRight;
}
style.paddingTop = Math.max(style.paddingTop, paddingTop);
style.paddingBottom = Math.max(style.paddingBottom, paddingBottom);
style.paddingLeft = Math.max(style.paddingLeft, paddingLeft);
style.paddingRight = Math.max(style.paddingRight, paddingRight);
return style;
};
_getViewStyles = () => {
const { viewWidth } = this.state;
// get padding values from style to add back in after insets are determined
// default precedence: padding[Side] -> vertical | horizontal -> padding -> 0
let {
padding = 0,
paddingVertical = padding,
paddingHorizontal = padding,
paddingTop = paddingVertical,
paddingBottom = paddingVertical,
paddingLeft = paddingHorizontal,
paddingRight = paddingHorizontal,
...viewStyle
} = StyleSheet.flatten(this.props.style || {});
if (typeof paddingTop !== 'number') {
paddingTop = doubleFromPercentString(paddingTop) * viewWidth;
}
if (typeof paddingBottom !== 'number') {
paddingBottom = doubleFromPercentString(paddingBottom) * viewWidth;
}
if (typeof paddingLeft !== 'number') {
paddingLeft = doubleFromPercentString(paddingLeft) * viewWidth;
}
if (typeof paddingRight !== 'number') {
paddingRight = doubleFromPercentString(paddingRight) * viewWidth;
}
return {
paddingTop,
paddingBottom,
paddingLeft,
paddingRight,
viewStyle,
};
};
_getInset = key => {
const { isLandscape } = this.props;
switch (key) {
case 'horizontal':
case 'right':
case 'left': {
return isLandscape ? (isIPhoneX ? 44 : 0) : 0;
}
case 'vertical':
case 'top': {
return statusBarHeight(isLandscape);
}
case 'bottom': {
return isIPhoneX ? (isLandscape ? 24 : 34) : 0;
}
}
};
}
export default withOrientation(SafeView);

View File

@@ -59,7 +59,12 @@ function areRoutesShallowEqual(one, two) {
return shallowEqual(one, two);
}
export default function ScenesReducer(scenes, nextState, prevState) {
export default function ScenesReducer(
scenes,
nextState,
prevState,
descriptors
) {
if (prevState === nextState) {
return scenes;
}
@@ -80,12 +85,16 @@ export default function ScenesReducer(scenes, nextState, prevState) {
const nextKeys = new Set();
nextState.routes.forEach((route, index) => {
const key = SCENE_KEY_PREFIX + route.key;
let descriptor = descriptors && descriptors[route.key];
const scene = {
index,
isActive: false,
isStale: false,
key,
route,
descriptor,
};
invariant(
!nextKeys.has(key),
@@ -109,12 +118,16 @@ export default function ScenesReducer(scenes, nextState, prevState) {
if (freshScenes.has(key)) {
return;
}
const lastScene = scenes.find(scene => scene.route.key === route.key);
const descriptor = lastScene && lastScene.descriptor;
staleScenes.set(key, {
index,
isActive: false,
isStale: true,
key,
route,
descriptor,
});
});
}

View File

@@ -0,0 +1,66 @@
import * as React from 'react';
import { NativeModules } from 'react-native';
import StackViewLayout from './StackViewLayout';
import Transitioner from '../Transitioner';
import NavigationActions from '../../NavigationActions';
import TransitionConfigs from './StackViewTransitionConfigs';
const NativeAnimatedModule =
NativeModules && NativeModules.NativeAnimatedModule;
class StackView extends React.Component {
static defaultProps = {
navigationConfig: {
mode: 'card',
},
};
render() {
return (
<Transitioner
render={this._render}
configureTransition={this._configureTransition}
navigation={this.props.navigation}
descriptors={this.props.descriptors}
onTransitionStart={this.props.onTransitionStart}
onTransitionEnd={(lastTransition, transition) => {
const { onTransitionEnd, navigation } = this.props;
navigation.dispatch(
NavigationActions.completeTransition({
key: navigation.state.key,
})
);
onTransitionEnd && onTransitionEnd(lastTransition, transition);
}}
/>
);
}
_configureTransition = (transitionProps, prevTransitionProps) => {
return {
...TransitionConfigs.getTransitionConfig(
this.props.navigationConfig.transitionConfig,
transitionProps,
prevTransitionProps,
this.props.navigationConfig.mode === 'modal'
).transitionSpec,
useNativeDriver: !!NativeAnimatedModule,
};
};
_render = (transitionProps, lastTransitionProps) => {
const { screenProps, navigationConfig } = this.props;
return (
<StackViewLayout
{...navigationConfig}
screenProps={screenProps}
descriptors={this.props.descriptors}
transitionProps={transitionProps}
lastTransitionProps={lastTransitionProps}
/>
);
};
}
export default StackView;

View File

@@ -0,0 +1,546 @@
import * as React from 'react';
import Transitioner from './Transitioner2';
import NavigationActions from '../../NavigationActions';
import Transitions from './StackViewTransitions';
const NativeAnimatedModule =
NativeModules && NativeModules.NativeAnimatedModule;
import clamp from 'clamp';
import {
Animated,
StyleSheet,
PanResponder,
Platform,
View,
I18nManager,
Easing,
NativeModules,
} from 'react-native';
import Card from './StackViewCard';
// import Header from '../Header/Header2'; // WIP.. interpolation reconfiguration, fun!
import SceneView from '../SceneView';
import invariant from '../../utils/invariant';
import * as ReactNativeFeatures from '../../utils/ReactNativeFeatures';
const emptyFunction = () => {};
const EaseInOut = Easing.inOut(Easing.ease);
/**
* The max duration of the card animation in milliseconds after released gesture.
* The actual duration should be always less then that because the rest distance
* is always less then the full distance of the layout.
*/
const ANIMATION_DURATION = 500;
/**
* The gesture distance threshold to trigger the back behavior. For instance,
* `1/2` means that moving greater than 1/2 of the width of the screen will
* trigger a back action
*/
const POSITION_THRESHOLD = 1 / 2;
/**
* The threshold (in pixels) to start the gesture action.
*/
const RESPOND_THRESHOLD = 20;
/**
* The distance of touch start from the edge of the screen where the gesture will be recognized
*/
const GESTURE_RESPONSE_DISTANCE_HORIZONTAL = 25;
const GESTURE_RESPONSE_DISTANCE_VERTICAL = 135;
const animatedSubscribeValue = animatedValue => {
if (!animatedValue.__isNative) {
return;
}
if (Object.keys(animatedValue._listeners).length === 0) {
animatedValue.addListener(emptyFunction);
}
};
class StackViewLayout extends React.Component {
/**
* Used to identify the starting point of the position when the gesture starts, such that it can
* be updated according to its relative position. This means that a card can effectively be
* "caught"- If a gesture starts while a card is animating, the card does not jump into a
* corresponding location for the touch.
*/
_gestureStartValue = 0;
// tracks if a touch is currently happening
_isResponding = false;
/**
* immediateIndex is used to represent the expected index that we will be on after a
* transition. To achieve a smooth animation when swiping back, the action to go back
* doesn't actually fire until the transition completes. The immediateIndex is used during
* the transition so that gestures can be handled correctly. This is a work-around for
* cases when the user quickly swipes back several times.
*/
_immediateIndex = null;
// _panResponder = PanResponder.create({
// onPanResponderTerminate: () => {
// this._isResponding = false;
// this._reset(index, 0);
// },
// onPanResponderGrant: () => {
// position.stopAnimation((value: number) => {
// this._isResponding = true;
// this._gestureStartValue = value;
// });
// },
// onMoveShouldSetPanResponder: (event, gesture) => {
// if (index !== scene.index) {
// return false;
// }
// const immediateIndex =
// this._immediateIndex == null ? index : this._immediateIndex;
// const currentDragDistance = gesture[isVertical ? 'dy' : 'dx'];
// const currentDragPosition =
// event.nativeEvent[isVertical ? 'pageY' : 'pageX'];
// const axisLength = isVertical
// ? layout.height.__getValue()
// : layout.width.__getValue();
// const axisHasBeenMeasured = !!axisLength;
// // Measure the distance from the touch to the edge of the screen
// const screenEdgeDistance = gestureDirectionInverted
// ? axisLength - (currentDragPosition - currentDragDistance)
// : currentDragPosition - currentDragDistance;
// // Compare to the gesture distance relavant to card or modal
// const { options } = scene.descriptor;
// const {
// gestureResponseDistance: userGestureResponseDistance = {},
// } = options;
// const gestureResponseDistance = isVertical
// ? userGestureResponseDistance.vertical ||
// GESTURE_RESPONSE_DISTANCE_VERTICAL
// : userGestureResponseDistance.horizontal ||
// GESTURE_RESPONSE_DISTANCE_HORIZONTAL;
// // GESTURE_RESPONSE_DISTANCE is about 25 or 30. Or 135 for modals
// if (screenEdgeDistance > gestureResponseDistance) {
// // Reject touches that started in the middle of the screen
// return false;
// }
// const hasDraggedEnough =
// Math.abs(currentDragDistance) > RESPOND_THRESHOLD;
// const isOnFirstCard = immediateIndex === 0;
// const shouldSetResponder =
// hasDraggedEnough && axisHasBeenMeasured && !isOnFirstCard;
// return shouldSetResponder;
// },
// onPanResponderMove: (event, gesture) => {
// // Handle the moving touches for our granted responder
// const startValue = this._gestureStartValue;
// const axis = isVertical ? 'dy' : 'dx';
// const axisDistance = isVertical
// ? layout.height.__getValue()
// : layout.width.__getValue();
// const currentValue =
// (I18nManager.isRTL && axis === 'dx') !== gestureDirectionInverted
// ? startValue + gesture[axis] / axisDistance
// : startValue - gesture[axis] / axisDistance;
// const value = clamp(index - 1, currentValue, index);
// position.setValue(value);
// },
// onPanResponderTerminationRequest: () =>
// // Returning false will prevent other views from becoming responder while
// // the navigation view is the responder (mid-gesture)
// false,
// onPanResponderRelease: (event, gesture) => {
// if (!this._isResponding) {
// return;
// }
// this._isResponding = false;
// const immediateIndex =
// this._immediateIndex == null ? index : this._immediateIndex;
// // Calculate animate duration according to gesture speed and moved distance
// const axisDistance = isVertical
// ? layout.height.__getValue()
// : layout.width.__getValue();
// const movementDirection = gestureDirectionInverted ? -1 : 1;
// const movedDistance =
// movementDirection * gesture[isVertical ? 'dy' : 'dx'];
// const gestureVelocity =
// movementDirection * gesture[isVertical ? 'vy' : 'vx'];
// const defaultVelocity = axisDistance / ANIMATION_DURATION;
// const velocity = Math.max(Math.abs(gestureVelocity), defaultVelocity);
// const resetDuration = gestureDirectionInverted
// ? (axisDistance - movedDistance) / velocity
// : movedDistance / velocity;
// const goBackDuration = gestureDirectionInverted
// ? movedDistance / velocity
// : (axisDistance - movedDistance) / velocity;
// // To asyncronously get the current animated value, we need to run stopAnimation:
// position.stopAnimation(value => {
// // If the speed of the gesture release is significant, use that as the indication
// // of intent
// if (gestureVelocity < -0.5) {
// this._reset(immediateIndex, resetDuration);
// return;
// }
// if (gestureVelocity > 0.5) {
// this._goBack(immediateIndex, goBackDuration);
// return;
// }
// // Then filter based on the distance the screen was moved. Over a third of the way swiped,
// // and the back will happen.
// if (value <= index - POSITION_THRESHOLD) {
// this._goBack(immediateIndex, goBackDuration);
// } else {
// this._reset(immediateIndex, resetDuration);
// }
// });
// },
// });
_renderHeader(descriptor, headerMode) {
const { options } = descriptor;
const { header } = options;
if (typeof header !== 'undefined' && typeof header !== 'function') {
return header;
}
// const renderHeader = header || (props => <Header {...props} />);
const renderHeader = header || (props => null);
const {
headerLeftInterpolator,
headerTitleInterpolator,
headerRightInterpolator,
} = this._getTransitionConfig();
const {
mode,
transitionProps,
prevTransitionProps,
...passProps
} = this.props;
return renderHeader({
...passProps,
...transitionProps,
descriptor,
mode: headerMode,
transitionPreset: this._getHeaderTransitionPreset(),
leftInterpolator: headerLeftInterpolator,
titleInterpolator: headerTitleInterpolator,
rightInterpolator: headerRightInterpolator,
});
}
// eslint-disable-next-line class-methods-use-this
_animatedSubscribe(props) {
// Hack to make this work with native driven animations. We add a single listener
// so the JS value of the following animated values gets updated. We rely on
// some Animated private APIs and not doing so would require using a bunch of
// value listeners but we'd have to remove them to not leak and I'm not sure
// when we'd do that with the current structure we have. `stopAnimation` callback
// is also broken with native animated values that have no listeners so if we
// want to remove this we have to fix this too.
animatedSubscribeValue(props.layout.width);
animatedSubscribeValue(props.layout.height);
animatedSubscribeValue(props.position);
}
// _reset(resetToIndex, duration) {
// if (
// Platform.OS === 'ios' &&
// ReactNativeFeatures.supportsImprovedSpringAnimation()
// ) {
// Animated.spring(this.props.transitionProps.position, {
// toValue: resetToIndex,
// stiffness: 5000,
// damping: 600,
// mass: 3,
// useNativeDriver: this.props.transitionProps.position.__isNative,
// }).start();
// } else {
// Animated.timing(this.props.transitionProps.position, {
// toValue: resetToIndex,
// duration,
// easing: EaseInOut,
// useNativeDriver: this.props.transitionProps.position.__isNative,
// }).start();
// }
// }
// _goBack(backFromIndex, duration) {
// const { navigation, position, scenes } = this.props.transitionProps;
// const toValue = Math.max(backFromIndex - 1, 0);
// // set temporary index for gesture handler to respect until the action is
// // dispatched at the end of the transition.
// this._immediateIndex = toValue;
// const onCompleteAnimation = () => {
// this._immediateIndex = null;
// const backFromScene = scenes.find(s => s.index === toValue + 1);
// if (!this._isResponding && backFromScene) {
// navigation.dispatch(
// NavigationActions.back({
// key: backFromScene.route.key,
// immediate: true,
// })
// );
// }
// };
// if (
// Platform.OS === 'ios' &&
// ReactNativeFeatures.supportsImprovedSpringAnimation()
// ) {
// Animated.spring(position, {
// toValue,
// stiffness: 5000,
// damping: 600,
// mass: 3,
// useNativeDriver: position.__isNative,
// }).start(onCompleteAnimation);
// } else {
// Animated.timing(position, {
// toValue,
// duration,
// easing: EaseInOut,
// useNativeDriver: position.__isNative,
// }).start(onCompleteAnimation);
// }
// }
render() {
let floatingHeader = null;
const headerMode = this._getHeaderMode();
const {
navigation,
transition,
descriptor,
descriptors,
layout,
mode,
} = this.props;
if (headerMode === 'float') {
floatingHeader = this._renderHeader(descriptor, headerMode);
}
const { index, routes } = navigation.state;
const isVertical = mode === 'modal';
const { options } = descriptor;
const gestureDirectionInverted = options.gestureDirection === 'inverted';
const gesturesEnabled =
typeof options.gesturesEnabled === 'boolean'
? options.gesturesEnabled
: Platform.OS === 'ios';
// const handlers = gesturesEnabled ? this._panResponder.panHandlers : {};
const handlers = {};
const containerStyle = [
styles.container,
this._getTransitionConfig().containerStyle,
];
let forwardScene = null;
let backwardScene = null;
if (transition) {
const { fromDescriptor, toDescriptor } = transition;
const fromKey = fromDescriptor.key;
const toKey = toDescriptor.key;
const toIndex = navigation.state.routes.findIndex(r => r.key === toKey);
invariant(
toIndex !== -1,
`Could not find toIndex in navigation state for ${fromKey}`
);
const fromIndex = navigation.state.routes.findIndex(
r => r.key === fromKey
);
if (fromIndex == -1) {
// we are coming from a screen that is no longer in the stack
backwardScene = fromDescriptor;
} else if (toIndex > fromIndex) {
// presumably we are going doing a push.
backwardScene = fromDescriptor;
} else {
// we are navigating back, and the forward scene is on top
forwardScene = fromDescriptor;
}
} else if (index > 0) {
// when we aren't transitioning, render the previous screen in case we swipe back.
const previousKey = routes[index - 1].key;
const previousDescriptor = descriptors[previousKey];
backwardScene = previousDescriptor;
}
return (
<View {...handlers} style={containerStyle}>
<View style={styles.scenes}>
{backwardScene && this._renderScene(backwardScene, index - 1)}
{this._renderScene(descriptor, index)}
{forwardScene && this._renderScene(forwardScene, index + 1)}
</View>
{floatingHeader}
</View>
);
}
_getHeaderMode() {
if (this.props.headerMode) {
return this.props.headerMode;
}
if (Platform.OS === 'android' || this.props.mode === 'modal') {
return 'screen';
}
return 'float';
}
_getHeaderTransitionPreset() {
// On Android or with header mode screen, we always just use in-place,
// we ignore the option entirely (at least until we have other presets)
if (Platform.OS === 'android' || this._getHeaderMode() === 'screen') {
return 'fade-in-place';
}
// TODO: validations: 'fade-in-place' or 'uikit' are valid
if (this.props.headerTransitionPreset) {
return this.props.headerTransitionPreset;
} else {
return 'fade-in-place';
}
}
_renderInnerScene(descriptor) {
const { options, navigation, getComponent } = descriptor;
const SceneComponent = getComponent();
const { screenProps } = this.props;
const headerMode = this._getHeaderMode();
if (headerMode === 'screen') {
return (
<View style={styles.container}>
<View style={{ flex: 1 }}>
<SceneView
screenProps={screenProps}
navigation={navigation}
component={SceneComponent}
/>
</View>
{this._renderHeader(descriptor, headerMode)}
</View>
);
}
return (
<SceneView
screenProps={screenProps}
navigation={navigation}
component={SceneComponent}
/>
);
}
_getTransitionConfig = () => {
const isModal = this.props.mode === 'modal';
return Transitions.getTransitionConfig(
this.props.transitionConfig,
this.props,
isModal
);
};
_renderScene = (descriptor, index) => {
const { screenInterpolator } = this._getTransitionConfig();
const style =
screenInterpolator &&
screenInterpolator({ ...this.props, descriptor, index });
return (
<Card
{...this.props}
// providing this descriptor will override this.props.descriptor, to tell the card exactly which scene to render, instead of this.props.descriptor, which defines what scene is active
descriptor={descriptor}
index={index}
key={`card_${descriptor.key}`}
style={[style, this.props.cardStyle]}
>
{this._renderInnerScene(descriptor)}
</Card>
);
};
}
const styles = StyleSheet.create({
container: {
flex: 1,
// Header is physically rendered after scenes so that Header won't be
// covered by the shadows of the scenes.
// That said, we'd have use `flexDirection: 'column-reverse'` to move
// Header above the scenes.
flexDirection: 'column-reverse',
},
scenes: {
flex: 1,
},
});
class StackView extends React.Component {
static defaultProps = {
navigationConfig: {
mode: 'card',
},
};
render() {
return (
<Transitioner
render={this._render}
configureTransition={this._configureTransition}
navigation={this.props.navigation}
descriptors={this.props.descriptors}
// onTransitionStart={this.props.onTransitionStart}
// onTransitionEnd={(lastTransition, transition) => {
// const { onTransitionEnd, navigation } = this.props;
// navigation.dispatch(
// NavigationActions.completeTransition({
// key: navigation.state.key,
// })
// );
// onTransitionEnd && onTransitionEnd(lastTransition, transition);
// }}
/>
);
}
_configureTransition = transitionProps => {
return {
...Transitions.getTransitionConfig(
this.props.navigationConfig.transitionConfig,
transitionProps,
this.props.navigationConfig.mode === 'modal'
).transitionSpec,
useNativeDriver: !!NativeAnimatedModule,
};
};
_render = transitionProps => {
const { screenProps } = this.props;
return <StackViewLayout {...transitionProps} screenProps={screenProps} />;
};
}
export default StackView;

View File

@@ -1,9 +1,9 @@
import React from 'react';
import { Animated, StyleSheet } from 'react-native';
import createPointerEventsContainer from './PointerEventsContainer';
import createPointerEventsContainer from './createPointerEventsContainer';
/**
* Component that renders the scene as card for the <NavigationCardStack />.
* Component that renders the scene as card for the <StackView />.
*/
class Card extends React.Component {
render() {
@@ -22,16 +22,12 @@ class Card extends React.Component {
const styles = StyleSheet.create({
main: {
...StyleSheet.absoluteFillObject,
backgroundColor: '#E9E9EF',
bottom: 0,
left: 0,
position: 'absolute',
right: 0,
shadowColor: 'black',
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0.2,
shadowRadius: 5,
top: 0,
},
});

View File

@@ -0,0 +1,460 @@
import * as React from 'react';
import clamp from 'clamp';
import {
Animated,
StyleSheet,
PanResponder,
Platform,
View,
I18nManager,
Easing,
} from 'react-native';
import Card from './StackViewCard';
import Header from '../Header/Header';
import NavigationActions from '../../NavigationActions';
import SceneView from '../SceneView';
import TransitionConfigs from './StackViewTransitionConfigs';
import * as ReactNativeFeatures from '../../utils/ReactNativeFeatures';
const emptyFunction = () => {};
const EaseInOut = Easing.inOut(Easing.ease);
/**
* The max duration of the card animation in milliseconds after released gesture.
* The actual duration should be always less then that because the rest distance
* is always less then the full distance of the layout.
*/
const ANIMATION_DURATION = 500;
/**
* The gesture distance threshold to trigger the back behavior. For instance,
* `1/2` means that moving greater than 1/2 of the width of the screen will
* trigger a back action
*/
const POSITION_THRESHOLD = 1 / 2;
/**
* The threshold (in pixels) to start the gesture action.
*/
const RESPOND_THRESHOLD = 20;
/**
* The distance of touch start from the edge of the screen where the gesture will be recognized
*/
const GESTURE_RESPONSE_DISTANCE_HORIZONTAL = 25;
const GESTURE_RESPONSE_DISTANCE_VERTICAL = 135;
const animatedSubscribeValue = animatedValue => {
if (!animatedValue.__isNative) {
return;
}
if (Object.keys(animatedValue._listeners).length === 0) {
animatedValue.addListener(emptyFunction);
}
};
class StackViewLayout extends React.Component {
/**
* Used to identify the starting point of the position when the gesture starts, such that it can
* be updated according to its relative position. This means that a card can effectively be
* "caught"- If a gesture starts while a card is animating, the card does not jump into a
* corresponding location for the touch.
*/
_gestureStartValue = 0;
// tracks if a touch is currently happening
_isResponding = false;
/**
* immediateIndex is used to represent the expected index that we will be on after a
* transition. To achieve a smooth animation when swiping back, the action to go back
* doesn't actually fire until the transition completes. The immediateIndex is used during
* the transition so that gestures can be handled correctly. This is a work-around for
* cases when the user quickly swipes back several times.
*/
_immediateIndex = null;
_renderHeader(scene, headerMode) {
const { options } = scene.descriptor;
const { header } = options;
if (typeof header !== 'undefined' && typeof header !== 'function') {
return header;
}
const renderHeader = header || ((props: *) => <Header {...props} />);
const {
headerLeftInterpolator,
headerTitleInterpolator,
headerRightInterpolator,
} = this._getTransitionConfig();
const {
mode,
transitionProps,
prevTransitionProps,
...passProps
} = this.props;
return renderHeader({
...passProps,
...transitionProps,
scene,
mode: headerMode,
transitionPreset: this._getHeaderTransitionPreset(),
leftInterpolator: headerLeftInterpolator,
titleInterpolator: headerTitleInterpolator,
rightInterpolator: headerRightInterpolator,
});
}
// eslint-disable-next-line class-methods-use-this
_animatedSubscribe(props) {
// Hack to make this work with native driven animations. We add a single listener
// so the JS value of the following animated values gets updated. We rely on
// some Animated private APIs and not doing so would require using a bunch of
// value listeners but we'd have to remove them to not leak and I'm not sure
// when we'd do that with the current structure we have. `stopAnimation` callback
// is also broken with native animated values that have no listeners so if we
// want to remove this we have to fix this too.
animatedSubscribeValue(props.transitionProps.layout.width);
animatedSubscribeValue(props.transitionProps.layout.height);
animatedSubscribeValue(props.transitionProps.position);
}
_reset(resetToIndex, duration) {
if (
Platform.OS === 'ios' &&
ReactNativeFeatures.supportsImprovedSpringAnimation()
) {
Animated.spring(this.props.transitionProps.position, {
toValue: resetToIndex,
stiffness: 5000,
damping: 600,
mass: 3,
useNativeDriver: this.props.transitionProps.position.__isNative,
}).start();
} else {
Animated.timing(this.props.transitionProps.position, {
toValue: resetToIndex,
duration,
easing: EaseInOut,
useNativeDriver: this.props.transitionProps.position.__isNative,
}).start();
}
}
_goBack(backFromIndex, duration) {
const { navigation, position, scenes } = this.props.transitionProps;
const toValue = Math.max(backFromIndex - 1, 0);
// set temporary index for gesture handler to respect until the action is
// dispatched at the end of the transition.
this._immediateIndex = toValue;
const onCompleteAnimation = () => {
this._immediateIndex = null;
const backFromScene = scenes.find(s => s.index === toValue + 1);
if (!this._isResponding && backFromScene) {
navigation.dispatch(
NavigationActions.back({
key: backFromScene.route.key,
immediate: true,
})
);
}
};
if (
Platform.OS === 'ios' &&
ReactNativeFeatures.supportsImprovedSpringAnimation()
) {
Animated.spring(position, {
toValue,
stiffness: 5000,
damping: 600,
mass: 3,
useNativeDriver: position.__isNative,
}).start(onCompleteAnimation);
} else {
Animated.timing(position, {
toValue,
duration,
easing: EaseInOut,
useNativeDriver: position.__isNative,
}).start(onCompleteAnimation);
}
}
render() {
let floatingHeader = null;
const headerMode = this._getHeaderMode();
if (headerMode === 'float') {
floatingHeader = this._renderHeader(
this.props.transitionProps.scene,
headerMode
);
}
const {
transitionProps: { navigation, position, layout, scene, scenes },
mode,
} = this.props;
const { index } = navigation.state;
const isVertical = mode === 'modal';
const { options } = scene.descriptor;
const gestureDirectionInverted = options.gestureDirection === 'inverted';
const gesturesEnabled =
typeof options.gesturesEnabled === 'boolean'
? options.gesturesEnabled
: Platform.OS === 'ios';
const responder = !gesturesEnabled
? null
: PanResponder.create({
onPanResponderTerminate: () => {
this._isResponding = false;
this._reset(index, 0);
},
onPanResponderGrant: () => {
position.stopAnimation((value: number) => {
this._isResponding = true;
this._gestureStartValue = value;
});
},
onMoveShouldSetPanResponder: (event, gesture) => {
if (index !== scene.index) {
return false;
}
const immediateIndex =
this._immediateIndex == null ? index : this._immediateIndex;
const currentDragDistance = gesture[isVertical ? 'dy' : 'dx'];
const currentDragPosition =
event.nativeEvent[isVertical ? 'pageY' : 'pageX'];
const axisLength = isVertical
? layout.height.__getValue()
: layout.width.__getValue();
const axisHasBeenMeasured = !!axisLength;
// Measure the distance from the touch to the edge of the screen
const screenEdgeDistance = gestureDirectionInverted
? axisLength - (currentDragPosition - currentDragDistance)
: currentDragPosition - currentDragDistance;
// Compare to the gesture distance relavant to card or modal
const { options } = scene.descriptor;
const {
gestureResponseDistance: userGestureResponseDistance = {},
} = options;
const gestureResponseDistance = isVertical
? userGestureResponseDistance.vertical ||
GESTURE_RESPONSE_DISTANCE_VERTICAL
: userGestureResponseDistance.horizontal ||
GESTURE_RESPONSE_DISTANCE_HORIZONTAL;
// GESTURE_RESPONSE_DISTANCE is about 25 or 30. Or 135 for modals
if (screenEdgeDistance > gestureResponseDistance) {
// Reject touches that started in the middle of the screen
return false;
}
const hasDraggedEnough =
Math.abs(currentDragDistance) > RESPOND_THRESHOLD;
const isOnFirstCard = immediateIndex === 0;
const shouldSetResponder =
hasDraggedEnough && axisHasBeenMeasured && !isOnFirstCard;
return shouldSetResponder;
},
onPanResponderMove: (event, gesture) => {
// Handle the moving touches for our granted responder
const startValue = this._gestureStartValue;
const axis = isVertical ? 'dy' : 'dx';
const axisDistance = isVertical
? layout.height.__getValue()
: layout.width.__getValue();
const currentValue =
(I18nManager.isRTL && axis === 'dx') !== gestureDirectionInverted
? startValue + gesture[axis] / axisDistance
: startValue - gesture[axis] / axisDistance;
const value = clamp(index - 1, currentValue, index);
position.setValue(value);
},
onPanResponderTerminationRequest: () =>
// Returning false will prevent other views from becoming responder while
// the navigation view is the responder (mid-gesture)
false,
onPanResponderRelease: (event, gesture) => {
if (!this._isResponding) {
return;
}
this._isResponding = false;
const immediateIndex =
this._immediateIndex == null ? index : this._immediateIndex;
// Calculate animate duration according to gesture speed and moved distance
const axisDistance = isVertical
? layout.height.__getValue()
: layout.width.__getValue();
const movementDirection = gestureDirectionInverted ? -1 : 1;
const movedDistance =
movementDirection * gesture[isVertical ? 'dy' : 'dx'];
const gestureVelocity =
movementDirection * gesture[isVertical ? 'vy' : 'vx'];
const defaultVelocity = axisDistance / ANIMATION_DURATION;
const velocity = Math.max(
Math.abs(gestureVelocity),
defaultVelocity
);
const resetDuration = gestureDirectionInverted
? (axisDistance - movedDistance) / velocity
: movedDistance / velocity;
const goBackDuration = gestureDirectionInverted
? movedDistance / velocity
: (axisDistance - movedDistance) / velocity;
// To asyncronously get the current animated value, we need to run stopAnimation:
position.stopAnimation(value => {
// If the speed of the gesture release is significant, use that as the indication
// of intent
if (gestureVelocity < -0.5) {
this._reset(immediateIndex, resetDuration);
return;
}
if (gestureVelocity > 0.5) {
this._goBack(immediateIndex, goBackDuration);
return;
}
// Then filter based on the distance the screen was moved. Over a third of the way swiped,
// and the back will happen.
if (value <= index - POSITION_THRESHOLD) {
this._goBack(immediateIndex, goBackDuration);
} else {
this._reset(immediateIndex, resetDuration);
}
});
},
});
const handlers = gesturesEnabled ? responder.panHandlers : {};
const containerStyle = [
styles.container,
this._getTransitionConfig().containerStyle,
];
return (
<View {...handlers} style={containerStyle}>
<View style={styles.scenes}>
{scenes.map((s: *) => this._renderCard(s))}
</View>
{floatingHeader}
</View>
);
}
_getHeaderMode() {
if (this.props.headerMode) {
return this.props.headerMode;
}
if (Platform.OS === 'android' || this.props.mode === 'modal') {
return 'screen';
}
return 'float';
}
_getHeaderTransitionPreset() {
// On Android or with header mode screen, we always just use in-place,
// we ignore the option entirely (at least until we have other presets)
if (Platform.OS === 'android' || this._getHeaderMode() === 'screen') {
return 'fade-in-place';
}
// TODO: validations: 'fade-in-place' or 'uikit' are valid
if (this.props.headerTransitionPreset) {
return this.props.headerTransitionPreset;
} else {
return 'fade-in-place';
}
}
_renderInnerScene(scene) {
const { options, navigation, getComponent } = scene.descriptor;
const SceneComponent = getComponent();
const { screenProps } = this.props;
const headerMode = this._getHeaderMode();
if (headerMode === 'screen') {
return (
<View style={styles.container}>
<View style={{ flex: 1 }}>
<SceneView
screenProps={screenProps}
navigation={navigation}
component={SceneComponent}
/>
</View>
{this._renderHeader(scene, headerMode)}
</View>
);
}
return (
<SceneView
screenProps={screenProps}
navigation={navigation}
component={SceneComponent}
/>
);
}
_getTransitionConfig = () => {
const isModal = this.props.mode === 'modal';
return TransitionConfigs.getTransitionConfig(
this.props.transitionConfig,
this.props.transitionProps,
this.props.prevTransitionProps,
isModal
);
};
_renderCard = scene => {
const { screenInterpolator } = this._getTransitionConfig();
const style =
screenInterpolator &&
screenInterpolator({ ...this.props.transitionProps, scene });
return (
<Card
{...this.props.transitionProps}
key={`card_${scene.key}`}
style={[style, this.props.cardStyle]}
scene={scene}
>
{this._renderInnerScene(scene)}
</Card>
);
};
}
const styles = StyleSheet.create({
container: {
flex: 1,
// Header is physically rendered after scenes so that Header won't be
// covered by the shadows of the scenes.
// That said, we'd have use `flexDirection: 'column-reverse'` to move
// Header above the scenes.
flexDirection: 'column-reverse',
},
scenes: {
flex: 1,
},
});
export default StackViewLayout;

View File

@@ -159,17 +159,9 @@ function forFade(props) {
};
}
function canUseNativeDriver() {
// The native driver can be enabled for this interpolator animating
// opacity, translateX, and translateY is supported by the native animation
// driver on iOS and Android.
return true;
}
export default {
forHorizontal,
forVertical,
forFadeFromBottomAndroid,
forFade,
canUseNativeDriver,
};

View File

@@ -1,5 +1,5 @@
import { Animated, Easing, Platform } from 'react-native';
import CardStackStyleInterpolator from './CardStackStyleInterpolator';
import StyleInterpolator from './StackViewStyleInterpolator';
import * as ReactNativeFeatures from '../../utils/ReactNativeFeatures';
let IOSTransitionSpec;
@@ -23,7 +23,7 @@ if (ReactNativeFeatures.supportsImprovedSpringAnimation()) {
// Standard iOS navigation transition
const SlideFromRightIOS = {
transitionSpec: IOSTransitionSpec,
screenInterpolator: CardStackStyleInterpolator.forHorizontal,
screenInterpolator: StyleInterpolator.forHorizontal,
containerStyle: {
backgroundColor: '#000',
},
@@ -32,7 +32,7 @@ const SlideFromRightIOS = {
// Standard iOS navigation transition for modals
const ModalSlideFromBottomIOS = {
transitionSpec: IOSTransitionSpec,
screenInterpolator: CardStackStyleInterpolator.forVertical,
screenInterpolator: StyleInterpolator.forVertical,
containerStyle: {
backgroundColor: '#000',
},
@@ -46,7 +46,7 @@ const FadeInFromBottomAndroid = {
easing: Easing.out(Easing.poly(5)), // decelerate
timing: Animated.timing,
},
screenInterpolator: CardStackStyleInterpolator.forFadeFromBottomAndroid,
screenInterpolator: StyleInterpolator.forFadeFromBottomAndroid,
};
// Standard Android navigation transition when closing an Activity
@@ -57,27 +57,22 @@ const FadeOutToBottomAndroid = {
easing: Easing.in(Easing.poly(4)), // accelerate
timing: Animated.timing,
},
screenInterpolator: CardStackStyleInterpolator.forFadeFromBottomAndroid,
screenInterpolator: StyleInterpolator.forFadeFromBottomAndroid,
};
function defaultTransitionConfig(
// props for the new screen
transitionProps,
// props for the old screen
prevTransitionProps,
// whether we're animating in/out a modal screen
isModal
) {
function defaultTransitionConfig(transitionProps, isModal) {
if (Platform.OS === 'android') {
// Use the default Android animation no matter if the screen is a modal.
// Android doesn't have full-screen modals like iOS does, it has dialogs.
if (
prevTransitionProps &&
transitionProps.index < prevTransitionProps.index
) {
// Navigating back to the previous screen
return FadeOutToBottomAndroid;
}
// todo, uncomment and fix, stop using prevTransitionProps
// // Use the default Android animation no matter if the screen is a modal.
// // Android doesn't have full-screen modals like iOS does, it has dialogs.
// if (
// prevTransitionProps &&
// transitionProps.index < prevTransitionProps.index
// ) {
// // Navigating back to the previous screen
// return FadeOutToBottomAndroid;
// }
return FadeInFromBottomAndroid;
}
// iOS and other platforms
@@ -87,23 +82,12 @@ function defaultTransitionConfig(
return SlideFromRightIOS;
}
function getTransitionConfig(
transitionConfigurer,
// props for the new screen
transitionProps,
// props for the old screen
prevTransitionProps,
isModal
) {
const defaultConfig = defaultTransitionConfig(
transitionProps,
prevTransitionProps,
isModal
);
function getTransitionConfig(transitionConfigurer, transitionProps, isModal) {
const defaultConfig = defaultTransitionConfig(transitionProps, isModal);
if (transitionConfigurer) {
return {
...defaultConfig,
...transitionConfigurer(transitionProps, prevTransitionProps, isModal),
...transitionConfigurer(transitionProps, isModal),
};
}
return defaultConfig;

View File

@@ -0,0 +1,268 @@
import { Animated, Easing, Platform } from 'react-native';
import * as ReactNativeFeatures from '../../utils/ReactNativeFeatures';
import { I18nManager } from 'react-native';
import getSceneIndicesForInterpolationInputRange from '../../utils/getSceneIndicesForInterpolationInputRange';
/**
* Utility that builds the style for the card in the cards stack.
*
* +------------+
* +-+ |
* +-+ | |
* | | | |
* | | | Focused |
* | | | Card |
* | | | |
* +-+ | |
* +-+ |
* +------------+
*/
/**
* Render the initial style when the initial layout isn't measured yet.
*/
function forInitial(props) {
const { navigation, descriptor } = props;
const { state } = navigation;
const activeKey = state.routes[state.index].key;
const focused = descriptor.key === activeKey;
const opacity = focused ? 1 : 0;
// If not focused, move the card far away.
const translate = focused ? 0 : 1000000;
return {
opacity,
transform: [{ translateX: translate }, { translateY: translate }],
};
}
/**
* Standard iOS-style slide in from the right.
*/
function forHorizontal(props) {
const { layout, transition, navigation, index } = props;
const { state } = navigation;
if (!layout.isMeasured) {
return forInitial(props);
}
const first = index - 1;
const last = index + 1;
const opacity = transition
? transition.progress.interpolate({
inputRange: [first, first + 0.01, index, last - 0.01, last],
outputRange: [0, 1, 1, 0.85, 0],
})
: 1;
const width = layout.initWidth;
const translateX = transition
? transition.progress.interpolate({
inputRange: [first, index, last],
outputRange: I18nManager.isRTL
? [-width, 0, width * 0.3]
: [width, 0, width * -0.3],
})
: 0;
return {
opacity,
transform: [{ translateX }],
};
}
/**
* Standard iOS-style slide in from the bottom (used for modals).
*/
function forVertical(props) {
const { layout, transition, descriptor } = props;
if (!layout.isMeasured) {
return forInitial(props);
}
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
const { first, last } = interpolate;
const index = scene.index;
const opacity = transition.progress.interpolate({
inputRange: [first, first + 0.01, index, last - 0.01, last],
outputRange: [0, 1, 1, 0.85, 0],
});
const height = layout.initHeight;
const translateY = transition.progress.interpolate({
inputRange: [first, index, last],
outputRange: [height, 0, 0],
});
const translateX = 0;
return {
opacity,
transform: [{ translateX }, { translateY }],
};
}
/**
* Standard Android-style fade in from the bottom.
*/
function forFadeFromBottomAndroid(props) {
const { layout, position, scene } = props;
if (!layout.isMeasured) {
return forInitial(props);
}
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
const { first, last } = interpolate;
const index = scene.index;
const inputRange = [first, index, last - 0.01, last];
const opacity = position.interpolate({
inputRange,
outputRange: [0, 1, 1, 0],
});
const translateY = position.interpolate({
inputRange,
outputRange: [50, 0, 0, 0],
});
const translateX = 0;
return {
opacity,
transform: [{ translateX }, { translateY }],
};
}
/**
* fadeIn and fadeOut
*/
function forFade(props) {
const { layout, position, scene } = props;
if (!layout.isMeasured) {
return forInitial(props);
}
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return { opacity: 0 };
const { first, last } = interpolate;
const index = scene.index;
const opacity = position.interpolate({
inputRange: [first, index, last],
outputRange: [0, 1, 1],
});
return {
opacity,
};
}
const StyleInterpolator = {
forHorizontal,
forVertical,
forFadeFromBottomAndroid,
forFade,
};
let IOSTransitionSpec;
if (ReactNativeFeatures.supportsImprovedSpringAnimation()) {
// These are the exact values from UINavigationController's animation configuration
IOSTransitionSpec = {
timing: Animated.spring,
stiffness: 1000,
damping: 500,
mass: 3,
};
} else {
// This is an approximation of the IOS spring animation using a derived bezier curve
IOSTransitionSpec = {
duration: 500,
easing: Easing.bezier(0.2833, 0.99, 0.31833, 0.99),
timing: Animated.timing,
};
}
// Standard iOS navigation transition
const SlideFromRightIOS = {
transitionSpec: IOSTransitionSpec,
screenInterpolator: StyleInterpolator.forHorizontal,
containerStyle: {
backgroundColor: '#000',
},
};
// Standard iOS navigation transition for modals
const ModalSlideFromBottomIOS = {
transitionSpec: IOSTransitionSpec,
screenInterpolator: StyleInterpolator.forVertical,
containerStyle: {
backgroundColor: '#000',
},
};
// Standard Android navigation transition when opening an Activity
const FadeInFromBottomAndroid = {
// See http://androidxref.com/7.1.1_r6/xref/frameworks/base/core/res/res/anim/activity_open_enter.xml
transitionSpec: {
duration: 350,
easing: Easing.out(Easing.poly(5)), // decelerate
timing: Animated.timing,
},
screenInterpolator: StyleInterpolator.forFadeFromBottomAndroid,
};
// Standard Android navigation transition when closing an Activity
const FadeOutToBottomAndroid = {
// See http://androidxref.com/7.1.1_r6/xref/frameworks/base/core/res/res/anim/activity_close_exit.xml
transitionSpec: {
duration: 230,
easing: Easing.in(Easing.poly(4)), // accelerate
timing: Animated.timing,
},
screenInterpolator: StyleInterpolator.forFadeFromBottomAndroid,
};
function defaultTransitionConfig(transitionProps, isModal) {
if (Platform.OS === 'android') {
// todo, uncomment and fix, stop using prevTransitionProps
// // Use the default Android animation no matter if the screen is a modal.
// // Android doesn't have full-screen modals like iOS does, it has dialogs.
// if (
// prevTransitionProps &&
// transitionProps.index < prevTransitionProps.index
// ) {
// // Navigating back to the previous screen
// return FadeOutToBottomAndroid;
// }
return FadeInFromBottomAndroid;
}
// iOS and other platforms
if (isModal) {
return ModalSlideFromBottomIOS;
}
return SlideFromRightIOS;
}
function getTransitionConfig(transitionConfigurer, transitionProps, isModal) {
const defaultConfig = defaultTransitionConfig(transitionProps, isModal);
if (transitionConfigurer) {
return {
...defaultConfig,
...transitionConfigurer(transitionProps, isModal),
};
}
return defaultConfig;
}
export default {
defaultTransitionConfig,
getTransitionConfig,
StyleInterpolator,
};

View File

@@ -0,0 +1,225 @@
import React from 'react';
import { Animated, Easing, StyleSheet, View } from 'react-native';
import invariant from '../../utils/invariant';
// Used for all animations unless overriden
const DefaultTransitionSpec = {
duration: 250,
easing: Easing.inOut(Easing.ease),
timing: Animated.timing,
};
class Transitioner extends React.Component {
_isMounted = false;
componentDidMount() {
this._isMounted = true;
}
componentWillUnmount() {
this._isMounted = false;
}
static getDerivedStateFromProps = (props, lastState) => {
const { navigation, descriptors } = props;
const { state } = navigation;
const canGoBack = state.index > 0;
const activeKey = state.routes[state.index].key;
const descriptor = descriptors[activeKey];
if (!lastState) {
lastState = {
backProgress: canGoBack ? new Animaged.Value(1) : null,
descriptor,
descriptors,
navigation,
transition: null,
layout: {
height: new Animated.Value(0),
initHeight: 0,
initWidth: 0,
isMeasured: false,
width: new Animated.Value(0),
},
};
}
// const lastNavState = this.props.navigation.state;
const lastNavState = lastState.navigation.state;
const lastActiveKey = lastNavState.routes[lastNavState.index].key;
// const transitionFromKey =
// lastActiveKey !== activeKey ? lastActiveKey : null;
const transitionFromKey = state.transitioningFromKey;
const transitionFromDescriptor =
transitionFromKey &&
lastState.descriptor &&
lastState.descriptor.key === transitionFromKey;
// We can only perform a transition if we have been told to via state.transitioningFromKey, and if our previous descriptor matches, indicating that the transitioningFromKey is currently being presented.
if (transitionFromDescriptor) {
if (lastState.transition) {
// there is already a transition in progress.. Don't interrupt it!
// At the end of the transition, we will compare props and start again
return lastState;
}
return {
...lastState,
navigation,
descriptor,
backProgress: null,
transition: {
fromDescriptor: lastState.descriptor,
toDescriptor: descriptor,
progress: new Animated.Value(0),
},
};
}
// No transition is being performed. If the key has changed, present it immediately without transition
if (lastActiveKey !== activeKey) {
return {
...lastState,
backProgress: canGoBack ? new Animaged.Value(1) : null,
descriptor,
transition: null,
};
}
return lastState;
};
// React doesn't handle getDerivedStateFromProps yet, but the polyfill is simple..
state = Transitioner.getDerivedStateFromProps(this.props);
componentWillReceiveProps(nextProps) {
const nextState = Transitioner.getDerivedStateFromProps(
nextProps,
this.state
);
if (this.state !== nextState) {
this.setState(nextState);
}
}
_startTransition(transition) {
const { configureTransition } = this.props;
const { descriptors } = this.state;
const { progress, fromDescriptor, toDescriptor } = transition;
progress.setValue(0);
// get the transition spec.
// passing the new transitionProps format (this.state) into configureTransition is a breaking change that I haven't documented yet!
const transitionUserSpec =
(configureTransition && configureTransition(this.state)) || null;
const transitionSpec = {
...DefaultTransitionSpec,
...transitionUserSpec,
};
const { timing } = transitionSpec;
// mutating a prop, this is terrible!
// it was in the previous transitioner implementation, so I'm leaving it as-is for now:
delete transitionSpec.timing;
timing(progress, {
...transitionSpec,
toValue: 1,
}).start(didComplete => {
this._completeTransition(transition, didComplete);
});
}
_completeTransition(transition, didComplete) {
if (!this._isMounted) {
return;
}
const { progress, fromDescriptor, toDescriptor } = transition;
const { navigation, descriptors } = this.props;
const nextState = navigation.state;
const activeKey = nextState.routes[nextState.index].key;
const nextDescriptor =
descriptors[activeKey] || this.state.descriptors[activeKey];
if (activeKey !== toDescriptor.key) {
// The user has changed navigation states during the transition! This is known as a queued transition.
// Now we set state for a new transition to the current navigation state
this.setState({
navigation,
descriptors,
descriptor: nextDescriptor,
transition: {
fromDescriptor: toDescriptor,
toDescriptor: nextDescriptor,
progress: new Animated.Value(0),
},
backProgress: null,
});
return;
}
const canGoBack = navigation.state.index > 0;
// All transitions are complete. Reset to normal state:
this.setState({
navigation,
descriptors,
descriptor: nextDescriptor,
transition: null,
backProgress: canGoBack ? new Animated.Value(1) : null,
});
}
render() {
console.log('Rendering Transitioner', this.state);
return (
<View onLayout={this._onLayout} style={[styles.main]}>
{this.props.render(this.state)}
</View>
);
}
componentDidUpdate(lastProps, lastState) {
// start transition if it needs it
if (
this.state.transition &&
(!lastState.transition ||
lastState.transition.toDescriptor !==
this.state.transition.toDescriptor)
) {
this._startTransition(this.state.transition);
}
}
_onLayout = event => {
const lastLayout = this.state.layout;
const { height, width } = event.nativeEvent.layout;
if (lastLayout.initWidth === width && lastLayout.initHeight === height) {
return;
}
const layout = {
...lastLayout,
initHeight: height,
initWidth: width,
isMeasured: true,
};
layout.height.setValue(height);
layout.width.setValue(width);
this.setState({ layout });
};
}
const styles = StyleSheet.create({
main: {
flex: 1,
},
});
export default Transitioner;

View File

@@ -0,0 +1,42 @@
import React from 'react';
import invariant from '../../utils/invariant';
import AnimatedValueSubscription from '../AnimatedValueSubscription';
const MIN_POSITION_OFFSET = 0.01;
/**
* Create a higher-order component that automatically computes the
* `pointerEvents` property for a component whenever navigation position
* changes.
*/
export default function createPointerEventsContainer(Component) {
class Container extends React.Component {
render() {
return (
<Component {...this.props} pointerEvents={this._getPointerEvents()} />
);
}
_getPointerEvents() {
const { navigation, descriptor, transition } = this.props;
const { state } = navigation;
const descriptorIndex = navigation.state.routes.findIndex(
r => r.key === descriptor.key
);
if (descriptorIndex !== state.index) {
// The scene isn't focused.
return descriptorIndex > state.index ? 'box-only' : 'none';
}
if (transition) {
// The positon is still away from scene's index.
// Scene's children should not receive touches until the position
// is close enough to scene's index.
return 'box-only';
}
return 'auto';
}
}
return Container;
}

View File

@@ -7,8 +7,10 @@ import {
Platform,
Keyboard,
} from 'react-native';
import SafeAreaView from 'react-native-safe-area-view';
import TabBarIcon from './TabBarIcon';
import SafeAreaView from '../SafeAreaView';
import NavigationActions from '../../NavigationActions';
import withOrientation from '../withOrientation';
const majorVersion = parseInt(Platform.Version, 10);
@@ -62,6 +64,7 @@ class TabBarBottom extends React.PureComponent {
if (typeof label === 'string') {
return (
<Animated.Text
numberOfLines={1}
style={[
styles.label,
{ color },
@@ -174,6 +177,24 @@ class TabBarBottom extends React.PureComponent {
}
}
_handleTabPress = index => {
const { jumpToIndex, navigation } = this.props;
const currentIndex = navigation.state.index;
if (currentIndex === index) {
let childRoute = navigation.state.routes[index];
if (childRoute.hasOwnProperty('index') && childRoute.index > 0) {
navigation.dispatch(
NavigationActions.popToTop({ key: childRoute.key })
);
} else {
// TODO: do something to scroll to top
}
} else {
jumpToIndex(index);
}
};
render() {
const {
position,
@@ -233,8 +254,13 @@ class TabBarBottom extends React.PureComponent {
accessibilityLabel={accessibilityLabel}
onPress={() =>
onPress
? onPress({ previousScene, scene, jumpToIndex })
: jumpToIndex(index)
? onPress({
previousScene,
scene,
jumpToIndex,
defaultHandler: this._handleTabPress,
})
: this._handleTabPress(index)
}
>
<Animated.View style={[styles.tab, { backgroundColor }]}>

View File

@@ -53,12 +53,11 @@ const styles = StyleSheet.create({
// We render the icon twice at the same position on top of each other:
// active and inactive one, so we can fade between them:
// Cover the whole iconContainer:
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
alignItems: 'center',
alignSelf: 'center',
height: '100%',
justifyContent: 'center',
position: 'absolute',
width: '100%',
},
});

View File

@@ -91,7 +91,15 @@ export default class TabBarTop extends React.PureComponent {
const onPress = getOnPress(previousScene, scene);
if (onPress) {
onPress({ previousScene, scene, jumpToIndex });
// To maintain the same API as `TabbarBottom`, we pass in a `defaultHandler`
// even though I don't believe in this case it should be any different
// than `jumpToIndex`.
onPress({
previousScene,
scene,
jumpToIndex,
defaultHandler: jumpToIndex,
});
} else {
jumpToIndex(scene.index);
}

View File

@@ -1,13 +1,14 @@
import React from 'react';
import { View, StyleSheet, Platform } from 'react-native';
import { TabViewAnimated, TabViewPagerPan } from 'react-native-tab-view';
import SafeAreaView from 'react-native-safe-area-view';
import SceneView from '../SceneView';
import withCachedChildNavigation from '../../withCachedChildNavigation';
import SafeAreaView from '../SafeAreaView';
import ResourceSavingSceneView from '../ResourceSavingSceneView';
class TabView extends React.PureComponent {
static defaultProps = {
lazy: true,
removedClippedSubviews: true,
// fix for https://github.com/react-native-community/react-native-tab-view/issues/312
initialLayout: Platform.select({
android: { width: 1, height: 0 },
@@ -20,27 +21,33 @@ class TabView extends React.PureComponent {
};
_renderScene = ({ route }) => {
const { screenProps } = this.props;
const childNavigation = this.props.childNavigationProps[route.key];
const TabComponent = this.props.router.getComponentForRouteName(
route.routeName
);
const { screenProps, descriptors } = this.props;
const {
lazy,
removeClippedSubviews,
animationEnabled,
swipeEnabled,
} = this.props.navigationConfig;
const descriptor = descriptors[route.key];
const TabComponent = descriptor.getComponent();
return (
<View style={styles.page}>
<SceneView
screenProps={screenProps}
component={TabComponent}
navigation={childNavigation}
/>
</View>
<ResourceSavingSceneView
lazy={lazy}
removeClippedSubViews={removeClippedSubviews}
animationEnabled={animationEnabled}
swipeEnabled={swipeEnabled}
screenProps={screenProps}
component={TabComponent}
navigation={this.props.navigation}
childNavigation={descriptor.navigation}
/>
);
};
_getLabel = ({ route, tintColor, focused }) => {
const options = this.props.router.getScreenOptions(
this.props.childNavigationProps[route.key],
this.props.screenProps || {}
);
const { screenProps, descriptors } = this.props;
const descriptor = descriptors[route.key];
const options = descriptor.options;
if (options.tabBarLabel) {
return typeof options.tabBarLabel === 'function'
@@ -56,19 +63,17 @@ class TabView extends React.PureComponent {
};
_getOnPress = (previousScene, { route }) => {
const options = this.props.router.getScreenOptions(
this.props.childNavigationProps[route.key],
this.props.screenProps || {}
);
const { descriptors } = this.props;
const descriptor = descriptors[route.key];
const options = descriptor.options;
return options.tabBarOnPress;
};
_getTestIDProps = ({ route, focused }) => {
const options = this.props.router.getScreenOptions(
this.props.childNavigationProps[route.key],
this.props.screenProps || {}
);
_getTestIDProps = ({ route }) => {
const { descriptors } = this.props;
const descriptor = descriptors[route.key];
const options = descriptor.options;
return typeof options.tabBarTestIDProps === 'function'
? options.tabBarTestIDProps({ focused })
@@ -76,10 +81,10 @@ class TabView extends React.PureComponent {
};
_renderIcon = ({ focused, route, tintColor }) => {
const options = this.props.router.getScreenOptions(
this.props.childNavigationProps[route.key],
this.props.screenProps || {}
);
const { descriptors } = this.props;
const descriptor = descriptors[route.key];
const options = descriptor.options;
if (options.tabBarIcon) {
return typeof options.tabBarIcon === 'function'
? options.tabBarIcon({ tintColor, focused })
@@ -93,7 +98,8 @@ class TabView extends React.PureComponent {
tabBarOptions,
tabBarComponent: TabBarComponent,
animationEnabled,
} = this.props;
tabBarPosition,
} = this.props.navigationConfig;
if (typeof TabBarComponent === 'undefined') {
return null;
}
@@ -102,7 +108,7 @@ class TabView extends React.PureComponent {
<TabBarComponent
{...props}
{...tabBarOptions}
tabBarPosition={this.props.tabBarPosition}
tabBarPosition={tabBarPosition}
screenProps={this.props.screenProps}
navigation={this.props.navigation}
getLabel={this._getLabel}
@@ -118,33 +124,35 @@ class TabView extends React.PureComponent {
render() {
const {
router,
tabBarComponent,
tabBarPosition,
animationEnabled,
configureTransition,
initialLayout,
screenProps,
} = this.props;
} = this.props.navigationConfig;
let renderHeader;
let renderFooter;
let renderPager;
const { state } = this.props.navigation;
const options = router.getScreenOptions(
this.props.childNavigationProps[state.routes[state.index].key],
screenProps || {}
);
const route = state.routes[state.index];
const { descriptors } = this.props;
const descriptor = descriptors[route.key];
const options = descriptor.options;
const tabBarVisible =
options.tabBarVisible == null ? true : options.tabBarVisible;
const swipeEnabled =
let swipeEnabled =
options.swipeEnabled == null
? this.props.swipeEnabled
? this.props.navigationConfig.swipeEnabled
: options.swipeEnabled;
if (typeof swipeEnabled === 'function') {
swipeEnabled = swipeEnabled(state);
}
if (tabBarComponent !== undefined && tabBarVisible) {
if (tabBarPosition === 'bottom') {
renderFooter = this._renderTabBar;
@@ -171,7 +179,6 @@ class TabView extends React.PureComponent {
renderScene: this._renderScene,
onIndexChange: this._handlePageChanged,
navigationState: this.props.navigation.state,
screenProps: this.props.screenProps,
style: styles.container,
};
@@ -179,15 +186,10 @@ class TabView extends React.PureComponent {
}
}
export default withCachedChildNavigation(TabView);
export default TabView;
const styles = StyleSheet.create({
container: {
flex: 1,
},
page: {
flex: 1,
overflow: 'hidden',
},
});

View File

@@ -29,7 +29,12 @@ class Transitioner extends React.Component {
layout,
position: new Animated.Value(this.props.navigation.state.index),
progress: new Animated.Value(1),
scenes: NavigationScenesReducer([], this.props.navigation.state),
scenes: NavigationScenesReducer(
[],
this.props.navigation.state,
null,
this.props.descriptors
),
};
this._prevTransitionProps = null;
@@ -56,7 +61,8 @@ class Transitioner extends React.Component {
const nextScenes = NavigationScenesReducer(
this.state.scenes,
nextProps.navigation.state,
this.props.navigation.state
this.props.navigation.state,
nextProps.descriptors
);
if (nextScenes === this.state.scenes) {
@@ -179,9 +185,19 @@ class Transitioner extends React.Component {
const prevTransitionProps = this._prevTransitionProps;
this._prevTransitionProps = null;
const scenes = this.state.scenes.filter(isSceneNotStale);
const nextState = {
...this.state,
scenes: this.state.scenes.filter(isSceneNotStale),
/**
* Array.prototype.filter creates a new instance of an array
* even if there were no elements removed. There are cases when
* `this.state.scenes` will have no stale scenes (typically when
* pushing a new route). As a result, components that rely on this prop
* might enter an unnecessary render cycle.
*/
scenes:
this.state.scenes.length === scenes.length ? this.state.scenes : scenes,
};
this._transitionProps = buildTransitionProps(this.props, nextState);

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { View } from 'react-native';
import renderer from 'react-test-renderer';
import TabRouter from '../../routers/TabRouter';
import TabView from '../TabView/TabView';
import TabBarBottom from '../TabView/TabBarBottom';
@@ -12,21 +11,30 @@ const dummyEventSubscriber = (name, handler) => ({
describe('TabBarBottom', () => {
it('renders successfully', () => {
const route = { key: 's1', routeName: 's1' };
const navigation = {
state: {
index: 0,
routes: [{ key: 's1', routeName: 's1' }],
routes: [route],
},
addListener: dummyEventSubscriber,
};
const router = TabRouter({ s1: { screen: View } });
const rendered = renderer
.create(
<TabView
tabBarComponent={TabBarBottom}
navigation={navigation}
router={router}
navigationConfig={{}}
descriptors={{
s1: {
state: route,
key: route.key,
options: {},
navigation: { state: route },
getComponent: () => View,
},
}}
/>
)
.toJSON();

View File

@@ -20,125 +20,6 @@ exports[`TabBarBottom renders successfully 1`] = `
]
}
>
<View
collapsable={undefined}
style={undefined}
>
<View
collapsable={undefined}
onLayout={[Function]}
style={
Object {
"backgroundColor": "#F7F7F7",
"borderTopColor": "rgba(0, 0, 0, .3)",
"borderTopWidth": 0.5,
"flexDirection": "row",
"height": 49,
"paddingBottom": 0,
"paddingLeft": 0,
"paddingRight": 0,
"paddingTop": 0,
}
}
>
<View
accessibilityComponentType={undefined}
accessibilityLabel={undefined}
accessibilityTraits={undefined}
accessible={true}
collapsable={undefined}
hitSlop={undefined}
nativeID={undefined}
onLayout={undefined}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
"alignItems": "center",
"backgroundColor": "rgba(0, 0, 0, 0)",
"flex": 1,
}
}
testID={undefined}
>
<View
style={
Array [
Object {
"alignItems": "center",
"flex": 1,
},
Object {
"flexDirection": "column",
"justifyContent": "flex-end",
},
undefined,
]
}
>
<View
style={
Object {
"flexGrow": 1,
}
}
>
<View
collapsable={undefined}
style={
Object {
"alignItems": "center",
"bottom": 0,
"justifyContent": "center",
"left": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
<View
collapsable={undefined}
style={
Object {
"alignItems": "center",
"bottom": 0,
"justifyContent": "center",
"left": 0,
"opacity": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
</View>
<Text
accessible={true}
allowFontScaling={true}
collapsable={undefined}
ellipsizeMode="tail"
style={
Object {
"backgroundColor": "transparent",
"color": "rgba(52, 120, 246, 1)",
"fontSize": 10,
"marginBottom": 1.5,
"textAlign": "center",
}
}
>
s1
</Text>
</View>
</View>
</View>
</View>
<RCTScrollView
DEPRECATED_sendUpdatedChildFrames={false}
alwaysBounceHorizontal={false}
@@ -223,6 +104,8 @@ exports[`TabBarBottom renders successfully 1`] = `
testID={undefined}
>
<View
collapsable={false}
removeClippedSubviews={false}
style={
Object {
"flex": 1,
@@ -231,25 +114,24 @@ exports[`TabBarBottom renders successfully 1`] = `
}
>
<View
navigation={
style={
Object {
"addListener": [Function],
"dispatch": undefined,
"goBack": [Function],
"navigate": [Function],
"pop": [Function],
"popToTop": [Function],
"push": [Function],
"replace": [Function],
"setParams": [Function],
"state": Object {
"key": "s1",
"routeName": "s1",
},
"flex": 1,
}
}
screenProps={undefined}
/>
>
<View
navigation={
Object {
"state": Object {
"key": "s1",
"routeName": "s1",
},
}
}
screenProps={undefined}
/>
</View>
</View>
</View>
</RCTScrollContentView>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 379 B

After

Width:  |  Height:  |  Size: 491 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 623 B

After

Width:  |  Height:  |  Size: 786 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 379 B

After

Width:  |  Height:  |  Size: 491 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 534 B

After

Width:  |  Height:  |  Size: 966 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 732 B

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 760 B

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -3,16 +3,25 @@ import propTypes from 'prop-types';
import hoistStatics from 'hoist-non-react-statics';
export default function withNavigation(Component) {
const componentWithNavigation = (props, { navigation }) => (
<Component {...props} navigation={navigation} />
);
class ComponentWithNavigation extends React.Component {
static displayName = `withNavigation(${Component.displayName ||
Component.name})`;
const displayName = Component.displayName || Component.name;
componentWithNavigation.displayName = `withNavigation(${displayName})`;
static contextTypes = {
navigation: propTypes.object.isRequired,
};
componentWithNavigation.contextTypes = {
navigation: propTypes.object.isRequired,
};
render() {
const { navigation } = this.context;
return (
<Component
{...this.props}
navigation={navigation}
ref={this.props.onRef}
/>
);
}
}
return hoistStatics(componentWithNavigation, Component);
return hoistStatics(ComponentWithNavigation, Component);
}

Some files were not shown because too many files have changed in this diff Show More