Compare commits

..

21 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
67 changed files with 2564 additions and 1903 deletions

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-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!
See our [Contributing Guide](CONTRIBUTING.md)!
#### Is this the only library available for navigation?

View File

@@ -25,7 +25,6 @@ import MultipleDrawer from './MultipleDrawer';
import TabsInDrawer from './TabsInDrawer';
import ModalStack from './ModalStack';
import StacksInTabs from './StacksInTabs';
import SwitchWithStacks from './SwitchWithStacks';
import StacksOverTabs from './StacksOverTabs';
import StacksWithKeys from './StacksWithKeys';
import SimpleStack from './SimpleStack';
@@ -40,10 +39,6 @@ const ExampleInfo = {
name: 'Stack Example',
description: 'A card stack',
},
SwitchWithStacks: {
name: 'Switch Example',
description: 'A switch with stacks inside',
},
SimpleTabs: {
name: 'Tabs Example',
description: 'Tabs following platform conventions',
@@ -121,22 +116,21 @@ const ExampleInfo = {
};
const ExampleRoutes = {
SimpleStack,
SwitchWithStacks,
SimpleTabs,
Drawer,
SimpleStack: SimpleStack,
SimpleTabs: SimpleTabs,
Drawer: Drawer,
// MultipleDrawer: {
// screen: MultipleDrawer,
// },
StackWithHeaderPreset,
StackWithTranslucentHeader,
TabsInDrawer,
CustomTabs,
CustomTransitioner,
ModalStack,
StacksWithKeys,
StacksInTabs,
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',
@@ -311,7 +305,8 @@ const AppNavigator = StackNavigator(
}
);
export default () => <AppNavigator />;
// export default () => <AppNavigator />;
export default SimpleStack;
const styles = StyleSheet.create({
item: {

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

@@ -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

@@ -1,121 +0,0 @@
/**
* @flow
*/
import React from 'react';
import {
ActivityIndicator,
AsyncStorage,
Button,
StatusBar,
StyleSheet,
View,
} from 'react-native';
import { StackNavigator, SwitchNavigator } from 'react-navigation';
class SignInScreen extends React.Component<any, any> {
static navigationOptions = {
title: 'Please sign in',
};
render() {
return (
<View style={styles.container}>
<Button title="Sign in!" onPress={this._signInAsync} />
<Button
title="Go back to other examples"
onPress={() => this.props.navigation.goBack(null)}
/>
<StatusBar barStyle="default" />
</View>
);
}
_signInAsync = async () => {
await AsyncStorage.setItem('userToken', 'abc');
this.props.navigation.navigate('App');
};
}
class HomeScreen extends React.Component<any, any> {
static navigationOptions = {
title: 'Welcome to the app!',
};
render() {
return (
<View style={styles.container}>
<Button title="Show me more of the app" onPress={this._showMoreApp} />
<Button title="Actually, sign me out :)" onPress={this._signOutAsync} />
<StatusBar barStyle="default" />
</View>
);
}
_showMoreApp = () => {
this.props.navigation.navigate('Other');
};
_signOutAsync = async () => {
await AsyncStorage.clear();
this.props.navigation.navigate('Auth');
};
}
class OtherScreen extends React.Component<any, any> {
static navigationOptions = {
title: 'Lots of features here',
};
render() {
return (
<View style={styles.container}>
<Button title="I'm done, sign me out" onPress={this._signOutAsync} />
<StatusBar barStyle="default" />
</View>
);
}
_signOutAsync = async () => {
await AsyncStorage.clear();
this.props.navigation.navigate('Auth');
};
}
class LoadingScreen extends React.Component<any, any> {
componentDidMount() {
this._bootstrapAsync();
}
_bootstrapAsync = async () => {
const userToken = await AsyncStorage.getItem('userToken');
let initialRouteName = userToken ? 'App' : 'Auth';
this.props.navigation.navigate(initialRouteName);
};
render() {
return (
<View style={styles.container}>
<ActivityIndicator />
<StatusBar barStyle="default" />
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
});
const AppStack = StackNavigator({ Home: HomeScreen, Other: OtherScreen });
const AuthStack = StackNavigator({ SignIn: SignInScreen });
export default SwitchNavigator({
Loading: LoadingScreen,
App: AppStack,
Auth: AuthStack,
});

View File

@@ -26,7 +26,7 @@
"react": "16.2.0",
"react-native": "^0.52.0",
"react-navigation": "link:../..",
"react-navigation-redux-helpers": "^1.0.3",
"react-navigation-redux-helpers": "^1.0.0",
"react-redux": "^5.0.6",
"redux": "^3.7.2"
},

View File

@@ -2,7 +2,6 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { addNavigationHelpers, StackNavigator } from 'react-navigation';
import { initializeListeners } from 'react-navigation-redux-helpers';
import LoginScreen from '../components/LoginScreen';
import MainScreen from '../components/MainScreen';
@@ -21,10 +20,6 @@ class AppWithNavigationState extends React.Component {
nav: PropTypes.object.isRequired,
};
componentDidMount() {
initializeListeners('root', this.props.nav);
}
render() {
const { dispatch, nav } = this.props;
return (

View File

@@ -269,7 +269,8 @@ declare module 'react-navigation' {
declare export type NavigationComponent =
| NavigationScreenComponent<NavigationRoute, *, *>
| NavigationContainer<NavigationStateRoute, *, *>;
| NavigationContainer<*, *, *>
| any;
declare export type NavigationScreenComponent<
Route: NavigationRoute,
@@ -295,6 +296,7 @@ declare module 'react-navigation' {
} & NavigationScreenRouteConfig);
declare export type NavigationScreenRouteConfig =
| NavigationComponent
| {
screen: NavigationComponent,
}
@@ -378,20 +380,6 @@ declare module 'react-navigation' {
...NavigationStackRouterConfig,
|};
/**
* Switch Navigator
*/
declare export type NavigationSwitchRouterConfig = {|
initialRouteName?: string,
initialRouteParams?: NavigationParams,
paths?: NavigationPathsConfig,
navigationOptions?: NavigationScreenConfig<*>,
order?: Array<string>,
backBehavior?: 'none' | 'initialRoute', // defaults to `'none'`
resetOnBlur?: boolean, // defaults to `true`
|};
/**
* Tab Navigator
*/
@@ -403,6 +391,7 @@ declare module 'react-navigation' {
navigationOptions?: NavigationScreenConfig<*>,
// todo: type these as the real route names rather than 'string'
order?: Array<string>,
// Does the back button cause the router to switch to the initial tab
backBehavior?: 'none' | 'initialRoute', // defaults `initialRoute`
|};
@@ -515,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
*/
@@ -553,7 +519,7 @@ declare module 'react-navigation' {
declare export type NavigationContainerProps<S: {}, O: {}> = $Shape<{
uriPrefix?: string | RegExp,
onNavigationStateChange?: ?(
onNavigationStateChange?: (
NavigationState,
NavigationState,
NavigationAction
@@ -712,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',
@@ -763,23 +725,24 @@ declare module 'react-navigation' {
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,
@@ -809,21 +772,11 @@ declare module 'react-navigation' {
routeConfigs: NavigationRouteConfigMap,
config?: _TabNavigatorConfig
): NavigationContainer<*, *, *>;
declare type _SwitchNavigatorConfig = {|
...NavigationSwitchRouterConfig,
|};
declare export function SwitchNavigator(
routeConfigs: NavigationRouteConfigMap,
config?: _SwitchNavigatorConfig
): NavigationContainer<*, *, *>;
declare type _DrawerViewConfig = {|
drawerLockMode?: 'unlocked' | 'locked-closed' | 'locked-open',
drawerWidth?: number | (() => number),
drawerPosition?: 'left' | 'right',
drawerOpenRoute?: string,
drawerCloseRoute?: string,
drawerToggleRoute?: string,
contentComponent?: React$ElementType,
contentOptions?: {},
style?: ViewStyleProp,
@@ -925,14 +878,12 @@ declare module 'react-navigation' {
vertical?: _SafeAreaViewForceInsetValue,
horizontal?: _SafeAreaViewForceInsetValue,
},
children?: React$Node,
children: React$Node,
style?: AnimatedViewStyleProp,
};
declare export var SafeAreaView: React$ComponentType<_SafeAreaViewProps>;
declare export var Header: React$ComponentType<HeaderProps> & {
HEIGHT: number,
};
declare export var Header: React$ComponentType<HeaderProps>;
declare type _HeaderTitleProps = {
children: React$Node,
@@ -958,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,

View File

@@ -1,6 +1,6 @@
{
"name": "react-navigation",
"version": "1.5.2",
"version": "1.2.1",
"description": "Routing and navigation for your React Native apps",
"main": "src/react-navigation.js",
"repository": {

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;
@@ -107,6 +110,16 @@ const completeTransition = createAction(COMPLETE_TRANSITION, payload => ({
key: payload && payload.key,
}));
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
BACK,
@@ -120,6 +133,9 @@ export default {
SET_PARAMS,
URI,
COMPLETE_TRANSITION,
OPEN_DRAWER,
CLOSE_DRAWER,
TOGGLE_DRAWER,
// Action creators
back,
@@ -133,4 +149,7 @@ export default {
setParams,
uri,
completeTransition,
openDrawer,
closeDrawer,
toggleDrawer,
};

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

@@ -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

@@ -85,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

@@ -4,7 +4,6 @@
* Based on the 'action' events that get fired for this navigation state, this utility will fire
* focus and blur events for this child
*/
export default function getChildEventSubscriber(addListener, key) {
const actionSubscribers = new Set();
const willFocusSubscribers = new Set();
@@ -12,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':
@@ -44,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',
@@ -77,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;
@@ -134,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

@@ -4,7 +4,7 @@ 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';
@@ -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,59 +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 {
initialRouteKey,
initialRouteName,
initialRouteParams,
paths,
headerMode,
headerTransitionPreset,
mode,
cardStyle,
transitionConfig,
onTransitionStart,
onTransitionEnd,
navigationOptions,
} = stackConfig;
const stackRouterConfig = {
initialRouteKey,
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}
headerTransitionPreset={headerTransitionPreset}
mode={mode}
cardStyle={cardStyle}
transitionConfig={transitionConfig}
onTransitionStart={onTransitionStart}
onTransitionEnd={(lastTransition, transition) => {
const { state, dispatch } = props.navigation;
dispatch(NavigationActions.completeTransition({ key: state.key }));
onTransitionEnd && onTransitionEnd(lastTransition, transition);
}}
/>
)
);
return createNavigationContainer(navigator);
};

View File

@@ -1,15 +0,0 @@
import React from 'react';
import SwitchRouter from '../routers/SwitchRouter';
import SwitchView from '../views/SwitchView/SwitchView';
import createNavigationContainer from '../createNavigationContainer';
import createNavigator from '../navigators/createNavigator';
export default (routeConfigMap, switchConfig = {}) => {
const router = SwitchRouter(routeConfigMap, switchConfig);
const navigator = createNavigator(router, routeConfigMap, switchConfig)(
props => <SwitchView {...props} />
);
return createNavigationContainer(navigator);
};

View File

@@ -2,7 +2,7 @@ import React, { Component } from 'react';
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: {

View File

@@ -1,18 +0,0 @@
import React, { Component } from 'react';
import { View } from 'react-native';
import renderer from 'react-test-renderer';
import SwitchNavigator from '../SwitchNavigator';
const A = () => <View />;
const B = () => <View />;
const routeConfig = { A, B };
describe('SwitchNavigator', () => {
it('renders successfully', () => {
const MySwitchNavigator = SwitchNavigator(routeConfig);
const rendered = renderer.create(<MySwitchNavigator />).toJSON();
expect(rendered).toMatchSnapshot();
});
});

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

@@ -48,7 +48,7 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
pointerEvents="auto"
style={
Object {
"backgroundColor": "#EFEFF4",
"backgroundColor": "#E9E9EF",
"bottom": 0,
"left": 0,
"opacity": 1,
@@ -234,7 +234,7 @@ exports[`StackNavigator renders successfully 1`] = `
pointerEvents="auto"
style={
Object {
"backgroundColor": "#EFEFF4",
"backgroundColor": "#E9E9EF",
"bottom": 0,
"left": 0,
"opacity": 1,

View File

@@ -1,3 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SwitchNavigator renders successfully 1`] = `<View />`;

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,42 +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,
lazy,
removeClippedSubviews,
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}
lazy={lazy}
removeClippedSubviews={removeClippedSubviews}
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,13 +20,10 @@ module.exports = {
return require('./navigators/createNavigator').default;
},
get StackNavigator() {
return require('./navigators/StackNavigator').default;
},
get SwitchNavigator() {
return require('./navigators/SwitchNavigator').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;
@@ -39,22 +36,16 @@ module.exports = {
get TabRouter() {
return require('./routers/TabRouter').default;
},
get SwitchRouter() {
return require('./routers/SwitchRouter').default;
},
// Views
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('react-native-safe-area-view').default;
@@ -90,11 +81,6 @@ module.exports = {
return require('./views/TabView/TabBarBottom').default;
},
// SwitchView
get SwitchView() {
return require('./views/SwitchView/SwitchView').default;
},
// HOCs
get withNavigation() {
return require('./views/withNavigation').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';
@@ -66,7 +65,7 @@ export default (routeConfigs, stackConfig = {}) => {
}
return {
key: 'StackRouterRoot',
isTransitioning: false,
transitioningFromKey: null,
index: 0,
routes: [
{
@@ -101,7 +100,7 @@ export default (routeConfigs, stackConfig = {}) => {
};
return {
key: 'StackRouterRoot',
isTransitioning: false,
transitioningFromKey: false,
index: 0,
routes: [route],
};
@@ -158,6 +157,7 @@ export default (routeConfigs, stackConfig = {}) => {
if (!state) {
return getInitialState(action);
}
const lastRouteKey = state.routes[state.index].key;
// Check if the focused child scene wants to handle the action, as long as
// it is not a reset to the root stack
@@ -222,13 +222,13 @@ 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,
};
@@ -254,7 +254,7 @@ export default (routeConfigs, stackConfig = {}) => {
}
return {
...StateUtils.push(state, route),
isTransitioning: action.immediate !== true,
transitioningFromKey: action.immediate !== true ? lastRouteKey : null,
};
} else if (
action.type === NavigationActions.PUSH &&
@@ -321,7 +321,7 @@ export default (routeConfigs, stackConfig = {}) => {
} else {
return {
...state,
isTransitioning: action.immediate !== true,
lastRouteKey: action.immediate !== true ? lastRouteKey : null,
index: 0,
routes: [state.routes[0]],
};
@@ -358,11 +358,11 @@ export default (routeConfigs, stackConfig = {}) => {
if (
action.type === NavigationActions.COMPLETE_TRANSITION &&
(action.key == null || action.key === state.key) &&
state.isTransitioning
state.transitioningFromKey
) {
return {
...state,
isTransitioning: false,
transitioningFromKey: null,
};
}
@@ -441,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 &&
@@ -577,7 +577,5 @@ export default (routeConfigs, stackConfig = {}) => {
routeConfigs,
stackConfig.navigationOptions
),
getScreenConfig: getScreenConfigDeprecated,
};
};

View File

@@ -1,360 +0,0 @@
import invariant from '../utils/invariant';
import getScreenForRouteName from './getScreenForRouteName';
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
validateRouteConfigMap(routeConfigs);
const order = config.order || Object.keys(routeConfigs);
const paths = config.paths || {};
const initialRouteParams = config.initialRouteParams;
const initialRouteName = config.initialRouteName || order[0];
const backBehavior = config.backBehavior || 'none';
const shouldBackNavigateToInitialRoute = backBehavior === 'initialRoute';
const resetOnBlur = config.hasOwnProperty('resetOnBlur')
? config.resetOnBlur
: true;
const initialRouteIndex = order.indexOf(initialRouteName);
const childRouters = {};
order.forEach(routeName => {
const routeConfig = routeConfigs[routeName];
paths[routeName] =
typeof routeConfig.path === 'string' ? routeConfig.path : routeName;
childRouters[routeName] = null;
const screen = getScreenForRouteName(routeConfigs, routeName);
if (screen.router) {
childRouters[routeName] = screen.router;
}
});
if (initialRouteIndex === -1) {
throw new Error(
`Invalid initialRouteName '${initialRouteName}'.` +
`Should be one of ${order.map(n => `"${n}"`).join(', ')}`
);
}
function resetChildRoute(routeName) {
const params =
routeName === initialRouteName ? initialRouteParams : undefined;
const childRouter = childRouters[routeName];
if (childRouter) {
const childAction = NavigationActions.init();
return {
...childRouter.getStateForAction(childAction),
key: routeName,
routeName,
params,
};
}
return {
key: routeName,
routeName,
params,
};
}
return {
getInitialState() {
const routes = order.map(resetChildRoute);
return {
routes,
index: initialRouteIndex,
isTransitioning: false,
};
},
getNextState(prevState, possibleNextState) {
let nextState;
if (prevState.index !== possibleNextState.index && resetOnBlur) {
const prevRouteName = prevState.routes[prevState.index].routeName;
const nextRoutes = [...possibleNextState.routes];
nextRoutes[prevState.index] = resetChildRoute(prevRouteName);
return {
...possibleNextState,
routes: nextRoutes,
};
} else {
nextState = possibleNextState;
}
return nextState;
},
getStateForAction(action, inputState) {
let prevState = inputState ? { ...inputState } : inputState;
let state = inputState || this.getInitialState();
let activeChildIndex = state.index;
if (action.type === NavigationActions.INIT) {
// NOTE(brentvatne): this seems weird... why are we merging these
// params into child routes?
// ---------------------------------------------------------------
// Merge any params from the action into all the child routes
const { params } = action;
if (params) {
state.routes = state.routes.map(route => ({
...route,
params: {
...route.params,
...params,
...(route.routeName === initialRouteName
? initialRouteParams
: null),
},
}));
}
}
// Let the current child handle it
const activeChildLastState = state.routes[state.index];
const activeChildRouter = childRouters[order[state.index]];
if (activeChildRouter) {
const activeChildState = activeChildRouter.getStateForAction(
action,
activeChildLastState
);
if (!activeChildState && inputState) {
return null;
}
if (activeChildState && activeChildState !== activeChildLastState) {
const routes = [...state.routes];
routes[state.index] = activeChildState;
return this.getNextState(prevState, {
...state,
routes,
});
}
}
// Handle tab changing. Do this after letting the current tab try to
// handle the action, to allow inner children to change first
if (backBehavior !== 'none') {
const isBackEligible =
action.key == null || action.key === activeChildLastState.key;
if (action.type === NavigationActions.BACK) {
if (isBackEligible && shouldBackNavigateToInitialRoute) {
activeChildIndex = initialRouteIndex;
} else {
return state;
}
}
}
let didNavigate = false;
if (action.type === NavigationActions.NAVIGATE) {
const navigateAction = action;
didNavigate = !!order.find((childId, i) => {
if (childId === navigateAction.routeName) {
activeChildIndex = i;
return true;
}
return false;
});
if (didNavigate) {
const childState = state.routes[activeChildIndex];
let newChildState;
const childRouter = childRouters[action.routeName];
if (action.action) {
newChildState = childRouter
? childRouter.getStateForAction(action.action, childState)
: null;
} else if (!childRouter && action.params) {
newChildState = {
...childState,
params: {
...(childState.params || {}),
...action.params,
},
};
}
if (newChildState && newChildState !== childState) {
const routes = [...state.routes];
routes[activeChildIndex] = newChildState;
return this.getNextState(prevState, {
...state,
routes,
index: activeChildIndex,
});
}
}
}
if (action.type === NavigationActions.SET_PARAMS) {
const key = action.key;
const lastRoute = state.routes.find(route => route.key === key);
if (lastRoute) {
const params = {
...lastRoute.params,
...action.params,
};
const routes = [...state.routes];
routes[state.routes.indexOf(lastRoute)] = {
...lastRoute,
params,
};
return this.getNextState(prevState, {
...state,
routes,
});
}
}
if (activeChildIndex !== state.index) {
return this.getNextState(prevState, {
...state,
index: activeChildIndex,
});
} else if (didNavigate && !inputState) {
return state;
} else if (didNavigate) {
return null;
}
// Let other children handle it and switch to the first child that returns a new state
let index = state.index;
let routes = state.routes;
order.find((childId, i) => {
const childRouter = childRouters[childId];
if (i === index) {
return false;
}
let childState = routes[i];
if (childRouter) {
childState = childRouter.getStateForAction(action, childState);
}
if (!childState) {
index = i;
return true;
}
if (childState !== routes[i]) {
routes = [...routes];
routes[i] = childState;
index = i;
return true;
}
return false;
});
// Nested routers can be updated after switching children 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 this.getNextState(prevState, {
...state,
index,
routes,
});
}
return state;
},
getComponentForState(state) {
const routeName = state.routes[state.index].routeName;
invariant(
routeName,
`There is no route defined for index ${state.index}. Check that
that you passed in a navigation state with a valid tab/screen index.`
);
const childRouter = childRouters[routeName];
if (childRouter) {
return childRouter.getComponentForState(state.routes[state.index]);
}
return getScreenForRouteName(routeConfigs, routeName);
},
getComponentForRouteName(routeName) {
return getScreenForRouteName(routeConfigs, routeName);
},
getPathAndParamsForState(state) {
const route = state.routes[state.index];
const routeName = order[state.index];
const subPath = paths[routeName];
const screen = getScreenForRouteName(routeConfigs, routeName);
let path = subPath;
let params = route.params;
if (screen && screen.router) {
const stateRoute = route;
// If it has a router it's a navigator.
// If it doesn't have router it's an ordinary React component.
const child = screen.router.getPathAndParamsForState(stateRoute);
path = subPath ? `${subPath}/${child.path}` : child.path;
params = child.params ? { ...params, ...child.params } : params;
}
return {
path,
params,
};
},
/**
* Gets an optional action, based on a relative path and query params.
*
* This will return null if there is no action matched
*/
getActionForPathAndParams(path, params) {
return (
order
.map(childId => {
const parts = path.split('/');
const pathToTest = paths[childId];
if (parts[0] === pathToTest) {
const childRouter = childRouters[childId];
const action = NavigationActions.navigate({
routeName: childId,
});
if (childRouter && childRouter.getActionForPathAndParams) {
action.action = childRouter.getActionForPathAndParams(
parts.slice(1).join('/'),
params
);
} else if (params) {
action.params = params;
}
return action;
}
return null;
})
.find(action => !!action) ||
order
.map(childId => {
const childRouter = childRouters[childId];
return (
childRouter && childRouter.getActionForPathAndParams(path, params)
);
})
.find(action => !!action) ||
null
);
},
getScreenOptions: createConfigGetter(
routeConfigs,
config.navigationOptions
),
getScreenConfig: getScreenConfigDeprecated,
};
};

View File

@@ -4,7 +4,6 @@ import createConfigGetter from './createConfigGetter';
import NavigationActions from '../NavigationActions';
import validateRouteConfigMap from './validateRouteConfigMap';
import getScreenConfigDeprecated from './getScreenConfigDeprecated';
function childrenUpdateWithoutSwitchingIndex(actionType) {
return [
@@ -68,7 +67,7 @@ export default (routeConfigs, config = {}) => {
state = {
routes,
index: initialRouteIndex,
isTransitioning: false,
transitioningFromKey: null,
};
// console.log(`${order.join('-')}: Initial state`, {state});
}
@@ -323,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

@@ -135,7 +135,7 @@ test('Handles deep action', () => {
const state1 = TestRouter.getStateForAction({ type: NavigationActions.INIT });
const expectedState = {
index: 0,
isTransitioning: false,
transitioningFromKey: false,
key: 'StackRouterRoot',
routes: [
{

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,7 +353,7 @@ describe('StackRouter', () => {
const initState = TestRouter.getStateForAction(NavigationActions.init());
expect(initState).toEqual({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'StackRouterRoot',
routes: [{ key: 'id-0', routeName: 'foo' }],
});
@@ -494,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' } },
@@ -507,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
@@ -519,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', () => {
@@ -678,7 +678,7 @@ describe('StackRouter', () => {
const state = router.getStateForAction({ type: NavigationActions.INIT });
expect(state).toEqual({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'StackRouterRoot',
routes: [
{
@@ -706,7 +706,7 @@ describe('StackRouter', () => {
);
expect(state3).toEqual({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'StackRouterRoot',
routes: [
{
@@ -773,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,
@@ -781,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', () => {
@@ -803,7 +803,7 @@ describe('StackRouter', () => {
const state = router.getStateForAction({ type: NavigationActions.INIT });
expect(state).toEqual({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'StackRouterRoot',
routes: [
{
@@ -831,7 +831,7 @@ describe('StackRouter', () => {
);
expect(state3).toEqual({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'StackRouterRoot',
routes: [
{
@@ -905,7 +905,7 @@ describe('StackRouter', () => {
const state = router.getStateForAction({ type: NavigationActions.INIT });
expect(state).toEqual({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'StackRouterRoot',
routes: [
{
@@ -929,7 +929,7 @@ describe('StackRouter', () => {
const state = router.getStateForAction({ type: NavigationActions.INIT });
expect(state).toEqual({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'StackRouterRoot',
routes: [
{
@@ -1274,19 +1274,19 @@ describe('StackRouter', () => {
expect(state).toEqual({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'StackRouterRoot',
routes: [
{
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'id-2',
params: { code: 'test', foo: 'bar' },
routeName: 'main',
routes: [
{
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'id-1',
params: { code: 'test', foo: 'bar', id: '4' },
routeName: 'profile',
@@ -1333,19 +1333,19 @@ describe('StackRouter', () => {
expect(state2).toEqual({
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'StackRouterRoot',
routes: [
{
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'id-5',
params: { code: '', foo: 'bar' },
routeName: 'main',
routes: [
{
index: 0,
isTransitioning: false,
transitioningFromKey: null,
key: 'id-4',
params: { code: '', foo: 'bar', id: '4' },
routeName: 'profile',
@@ -1448,7 +1448,7 @@ describe('StackRouter', () => {
const state = {
index: 0,
isTransitioning: false,
transitioningFromKey: null,
routes: [
{
index: 1,
@@ -1664,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(
{
@@ -1675,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

@@ -1,109 +0,0 @@
/* eslint react/display-name:0 */
import React from 'react';
import SwitchRouter from '../SwitchRouter';
import StackRouter from '../StackRouter';
import NavigationActions from '../../NavigationActions';
describe('SwitchRouter', () => {
test('resets the route when unfocusing a tab by default', () => {
const router = getExampleRouter();
const state = router.getStateForAction({ type: NavigationActions.INIT });
const state2 = router.getStateForAction(
{ type: NavigationActions.NAVIGATE, routeName: 'A2' },
state
);
expect(state2.routes[0].index).toEqual(1);
expect(state2.routes[0].routes.length).toEqual(2);
const state3 = router.getStateForAction(
{ type: NavigationActions.NAVIGATE, routeName: 'B' },
state2
);
expect(state3.routes[0].index).toEqual(0);
expect(state3.routes[0].routes.length).toEqual(1);
});
test('does not reset the route on unfocus if resetOnBlur is false', () => {
const router = getExampleRouter({ resetOnBlur: false });
const state = router.getStateForAction({ type: NavigationActions.INIT });
const state2 = router.getStateForAction(
{ type: NavigationActions.NAVIGATE, routeName: 'A2' },
state
);
expect(state2.routes[0].index).toEqual(1);
expect(state2.routes[0].routes.length).toEqual(2);
const state3 = router.getStateForAction(
{ type: NavigationActions.NAVIGATE, routeName: 'B' },
state2
);
expect(state3.routes[0].index).toEqual(1);
expect(state3.routes[0].routes.length).toEqual(2);
});
test('ignores back by default', () => {
const router = getExampleRouter();
const state = router.getStateForAction({ type: NavigationActions.INIT });
const state2 = router.getStateForAction(
{ type: NavigationActions.NAVIGATE, routeName: 'B' },
state
);
expect(state2.index).toEqual(1);
const state3 = router.getStateForAction(
{ type: NavigationActions.BACK },
state2
);
expect(state3.index).toEqual(1);
});
test('handles back if given a backBehavior', () => {
const router = getExampleRouter({ backBehavior: 'initialRoute' });
const state = router.getStateForAction({ type: NavigationActions.INIT });
const state2 = router.getStateForAction(
{ type: NavigationActions.NAVIGATE, routeName: 'B' },
state
);
expect(state2.index).toEqual(1);
const state3 = router.getStateForAction(
{ type: NavigationActions.BACK },
state2
);
expect(state3.index).toEqual(0);
});
});
const getExampleRouter = (config = {}) => {
const PlainScreen = () => <div />;
const StackA = () => <div />;
const StackB = () => <div />;
StackA.router = StackRouter({
A1: PlainScreen,
A2: PlainScreen,
});
StackB.router = StackRouter({
B1: PlainScreen,
B2: PlainScreen,
});
const router = SwitchRouter(
{
A: StackA,
B: StackB,
},
{
initialRouteName: 'A',
...config,
}
);
return router;
};

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

@@ -1,82 +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, prevProps) => {
const {
screenProps,
headerMode,
headerTransitionPreset,
mode,
router,
cardStyle,
transitionConfig,
} = this.props;
return (
<CardStack
screenProps={screenProps}
headerMode={headerMode}
headerTransitionPreset={headerTransitionPreset}
mode={mode}
router={router}
cardStyle={cardStyle}
transitionConfig={transitionConfig}
transitionProps={props}
prevTransitionProps={prevProps}
/>
);
};
}
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,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

@@ -2,32 +2,21 @@ 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';
/**
* 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 }) => {
@@ -56,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)
@@ -86,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
@@ -94,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>
@@ -102,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,16 +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,
};
_childEventSubscribers = {};
componentWillMount() {
this._updateScreenNavigation(this.props.navigation);
Dimensions.addEventListener('change', this._updateWidth);
}
@@ -29,130 +25,60 @@ export default class DrawerView extends React.PureComponent {
Dimensions.removeEventListener('change', this._updateWidth);
}
componentDidUpdate() {
const activeKeys = this.props.navigation.state.routes.map(
route => route.key
);
Object.keys(this._childEventSubscribers).forEach(key => {
if (!activeKeys.includes(key)) {
delete this._childEventSubscribers[key];
}
});
}
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);
}
};
_isRouteFocused = route => () => {
const { state } = this.props.navigation;
const focusedRoute = state.routes[state.index];
return route === focusedRoute;
};
_updateScreenNavigation = navigation => {
const { drawerCloseRoute } = this.props;
const navigationState = navigation.state.routes.find(
route => route.routeName === drawerCloseRoute
);
if (
this._screenNavigationProp &&
this._screenNavigationProp.state === navigationState
) {
return;
}
if (!this._childEventSubscribers[navigationState.key]) {
this._childEventSubscribers[
navigationState.key
] = getChildEventSubscriber(navigation.addListener, navigationState.key);
}
this._screenNavigationProp = addNavigationHelpers({
dispatch: navigation.dispatch,
state: navigationState,
isFocused: this._isRouteFocused.bind(this, navigationState),
addListener: this._childEventSubscribers[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
@@ -161,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

@@ -47,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) {
@@ -63,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;
}
@@ -75,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.
@@ -127,8 +120,7 @@ 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
@@ -148,9 +140,15 @@ 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={this._navigateBack}
onPress={goBack}
pressColorAndroid={options.headerPressColorAndroid}
tintColor={options.headerTintColor}
buttonImage={options.headerBackImage}
@@ -167,7 +165,7 @@ class Header extends React.PureComponent {
ButtonContainerComponent,
LabelContainerComponent
) => {
const { options } = this.props.getScreenDetails(props.scene);
const { options } = props.scene.descriptor;
const backButtonTitle = this._getBackButtonTitleString(props.scene);
const truncatedBackButtonTitle = this._getTruncatedBackButtonTitle(
props.scene
@@ -193,13 +191,12 @@ 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) {
const { options } = this.props.getScreenDetails(props.scene);
const { options } = props.scene.descriptor;
const { transitionPreset } = this.props;
@@ -374,7 +371,7 @@ class Header extends React.PureComponent {
});
const { isLandscape, transitionPreset } = this.props;
const { options } = this.props.getScreenDetails(props.scene);
const { options } = props.scene.descriptor;
const wrapperProps = {
style: styles.header,
@@ -439,7 +436,7 @@ class Header extends React.PureComponent {
});
}
const { options } = this.props.getScreenDetails(scene);
const { options } = scene.descriptor;
const { headerStyle = {} } = options;
const headerStyleObj = StyleSheet.flatten(headerStyle);
const appBarHeight = getAppBarHeight(isLandscape);

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

@@ -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

@@ -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: {
backgroundColor: '#EFEFF4',
bottom: 0,
left: 0,
position: 'absolute',
right: 0,
...StyleSheet.absoluteFillObject,
backgroundColor: '#E9E9EF',
shadowColor: 'black',
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0.2,
shadowRadius: 5,
top: 0,
},
});

View File

@@ -1,4 +1,4 @@
import React from 'react';
import * as React from 'react';
import clamp from 'clamp';
import {
@@ -11,14 +11,12 @@ import {
Easing,
} from 'react-native';
import Card from './Card';
import Card from './StackViewCard';
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';
import TransitionConfigs from './StackViewTransitionConfigs';
import * as ReactNativeFeatures from '../../utils/ReactNativeFeatures';
const emptyFunction = () => {};
@@ -59,7 +57,7 @@ const animatedSubscribeValue = animatedValue => {
}
};
class CardStack extends React.Component {
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
@@ -80,76 +78,15 @@ class CardStack extends React.Component {
*/
_immediateIndex = null;
_screenDetails = {};
_childEventSubscribers = {};
componentWillReceiveProps(props) {
if (props.screenProps !== this.props.screenProps) {
this._screenDetails = {};
}
props.transitionProps.scenes.forEach(newScene => {
if (
this._screenDetails[newScene.key] &&
this._screenDetails[newScene.key].state !== newScene.route
) {
this._screenDetails[newScene.key] = null;
}
});
}
componentDidUpdate() {
const activeKeys = this.props.transitionProps.navigation.state.routes.map(
route => route.key
);
Object.keys(this._childEventSubscribers).forEach(key => {
if (!activeKeys.includes(key)) {
delete this._childEventSubscribers[key];
}
});
}
_isRouteFocused = route => {
const { state } = this.props.navigation;
const focusedRoute = state.routes[state.index];
return route === focusedRoute;
};
_getScreenDetails = scene => {
const { screenProps, transitionProps: { navigation }, router } = this.props;
let screenDetails = this._screenDetails[scene.key];
if (!screenDetails || screenDetails.state !== scene.route) {
if (!this._childEventSubscribers[scene.route.key]) {
this._childEventSubscribers[scene.route.key] = getChildEventSubscriber(
navigation.addListener,
scene.route.key
);
}
const screenNavigation = addNavigationHelpers({
dispatch: navigation.dispatch,
state: scene.route,
isFocused: this._isRouteFocused.bind(this, scene.route),
addListener: this._childEventSubscribers[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;
const { options } = scene.descriptor;
const { header } = options;
if (typeof header !== 'undefined' && typeof header !== 'function') {
return header;
}
const renderHeader = header || (props => <Header {...props} />);
const renderHeader = header || ((props: *) => <Header {...props} />);
const {
headerLeftInterpolator,
headerTitleInterpolator,
@@ -169,7 +106,6 @@ class CardStack extends React.Component {
scene,
mode: headerMode,
transitionPreset: this._getHeaderTransitionPreset(),
getScreenDetails: this._getScreenDetails,
leftInterpolator: headerLeftInterpolator,
titleInterpolator: headerTitleInterpolator,
rightInterpolator: headerRightInterpolator,
@@ -269,7 +205,8 @@ class CardStack extends React.Component {
} = this.props;
const { index } = navigation.state;
const isVertical = mode === 'modal';
const { options } = this._getScreenDetails(scene);
const { options } = scene.descriptor;
const gestureDirectionInverted = options.gestureDirection === 'inverted';
const gesturesEnabled =
@@ -285,7 +222,7 @@ class CardStack extends React.Component {
this._reset(index, 0);
},
onPanResponderGrant: () => {
position.stopAnimation(value => {
position.stopAnimation((value: number) => {
this._isResponding = true;
this._gestureStartValue = value;
});
@@ -309,9 +246,12 @@ class CardStack extends React.Component {
? axisLength - (currentDragPosition - currentDragDistance)
: currentDragPosition - currentDragDistance;
// Compare to the gesture distance relavant to card or modal
const { options } = scene.descriptor;
const {
gestureResponseDistance: userGestureResponseDistance = {},
} = this._getScreenDetails(scene).options;
} = options;
const gestureResponseDistance = isVertical
? userGestureResponseDistance.vertical ||
GESTURE_RESPONSE_DISTANCE_VERTICAL
@@ -412,7 +352,7 @@ class CardStack extends React.Component {
return (
<View {...handlers} style={containerStyle}>
<View style={styles.scenes}>
{scenes.map(s => this._renderCard(s))}
{scenes.map((s: *) => this._renderCard(s))}
</View>
{floatingHeader}
</View>
@@ -444,8 +384,10 @@ class CardStack extends React.Component {
}
}
_renderInnerScene(SceneComponent, scene) {
const { navigation } = this._getScreenDetails(scene);
_renderInnerScene(scene) {
const { options, navigation, getComponent } = scene.descriptor;
const SceneComponent = getComponent();
const { screenProps } = this.props;
const headerMode = this._getHeaderMode();
if (headerMode === 'screen') {
@@ -464,7 +406,7 @@ class CardStack extends React.Component {
}
return (
<SceneView
screenProps={this.props.screenProps}
screenProps={screenProps}
navigation={navigation}
component={SceneComponent}
/>
@@ -488,21 +430,14 @@ class CardStack extends React.Component {
screenInterpolator &&
screenInterpolator({ ...this.props.transitionProps, scene });
const SceneComponent = this.props.router.getComponentForRouteName(
scene.route.routeName
);
const { transitionProps, ...props } = this.props;
return (
<Card
{...props}
{...transitionProps}
{...this.props.transitionProps}
key={`card_${scene.key}`}
style={[style, this.props.cardStyle]}
scene={scene}
>
{this._renderInnerScene(SceneComponent, scene)}
{this._renderInnerScene(scene)}
</Card>
);
};
@@ -522,4 +457,4 @@ const styles = StyleSheet.create({
},
});
export default CardStack;
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

@@ -1,27 +0,0 @@
import React from 'react';
import SceneView from '../SceneView';
import withCachedChildNavigation from '../../withCachedChildNavigation';
class SwitchContainer extends React.Component {
render() {
const { screenProps } = this.props;
const route = this.props.navigation.state.routes[
this.props.navigation.state.index
];
const childNavigation = this.props.childNavigationProps[route.key];
const ChildComponent = this.props.router.getComponentForRouteName(
route.routeName
);
return (
<SceneView
component={ChildComponent}
navigation={childNavigation}
screenProps={screenProps}
/>
);
}
}
export default withCachedChildNavigation(SwitchContainer);

View File

@@ -100,7 +100,6 @@ class TabBarBottom extends React.PureComponent {
if (showIcon === false) {
return null;
}
return (
<TabBarIcon
position={position}
@@ -109,11 +108,7 @@ class TabBarBottom extends React.PureComponent {
inactiveTintColor={inactiveTintColor}
renderIcon={renderIcon}
scene={scene}
style={
showLabel && this._shouldUseHorizontalTabs()
? styles.horizontalIcon
: styles.icon
}
style={showLabel && this._shouldUseHorizontalTabs() ? {} : styles.icon}
/>
);
};
@@ -291,9 +286,6 @@ class TabBarBottom extends React.PureComponent {
}
}
const DEFAULT_HEIGHT = 49;
const COMPACT_HEIGHT = 29;
const styles = StyleSheet.create({
tabBar: {
backgroundColor: '#F7F7F7', // Default background color in iOS 10
@@ -302,10 +294,10 @@ const styles = StyleSheet.create({
flexDirection: 'row',
},
tabBarCompact: {
height: COMPACT_HEIGHT,
height: 29,
},
tabBarRegular: {
height: DEFAULT_HEIGHT,
height: 49,
},
tab: {
flex: 1,
@@ -322,9 +314,6 @@ const styles = StyleSheet.create({
icon: {
flexGrow: 1,
},
horizontalIcon: {
height: Platform.isPad ? DEFAULT_HEIGHT : COMPACT_HEIGHT,
},
label: {
textAlign: 'center',
backgroundColor: 'transparent',

View File

@@ -23,7 +23,6 @@ export default class TabBarIcon extends React.PureComponent {
inputRange,
outputRange: inputRange.map(i => (i === index ? 0 : 1)),
});
// We render the icon twice at the same position on top of each other:
// active and inactive one, so we can fade between them.
return (

View File

@@ -4,7 +4,6 @@ import { TabViewAnimated, TabViewPagerPan } from 'react-native-tab-view';
import SafeAreaView from 'react-native-safe-area-view';
import ResourceSavingSceneView from '../ResourceSavingSceneView';
import withCachedChildNavigation from '../../withCachedChildNavigation';
class TabView extends React.PureComponent {
static defaultProps = {
@@ -22,31 +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 (
<ResourceSavingSceneView
lazy={this.props.lazy}
removeClippedSubViews={this.props.removeClippedSubviews}
animationEnabled={this.props.animationEnabled}
swipeEnabled={this.props.swipeEnabled}
lazy={lazy}
removeClippedSubViews={removeClippedSubviews}
animationEnabled={animationEnabled}
swipeEnabled={swipeEnabled}
screenProps={screenProps}
component={TabComponent}
navigation={this.props.navigation}
childNavigation={childNavigation}
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'
@@ -62,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 })
@@ -82,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 })
@@ -99,7 +98,8 @@ class TabView extends React.PureComponent {
tabBarOptions,
tabBarComponent: TabBarComponent,
animationEnabled,
} = this.props;
tabBarPosition,
} = this.props.navigationConfig;
if (typeof TabBarComponent === 'undefined') {
return null;
}
@@ -108,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}
@@ -124,31 +124,29 @@ 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;
let swipeEnabled =
options.swipeEnabled == null
? this.props.swipeEnabled
? this.props.navigationConfig.swipeEnabled
: options.swipeEnabled;
if (typeof swipeEnabled === 'function') {
@@ -181,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,
};
@@ -189,7 +186,7 @@ class TabView extends React.PureComponent {
}
}
export default withCachedChildNavigation(TabView);
export default TabView;
const styles = StyleSheet.create({
container: {

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) {

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]}
pointerEvents="box-none"
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",
"alignSelf": "center",
"height": "100%",
"justifyContent": "center",
"opacity": 1,
"position": "absolute",
"width": "100%",
}
}
/>
<View
collapsable={undefined}
style={
Object {
"alignItems": "center",
"alignSelf": "center",
"height": "100%",
"justifyContent": "center",
"opacity": 0,
"position": "absolute",
"width": "100%",
}
}
/>
</View>
<Text
accessible={true}
allowFontScaling={true}
collapsable={undefined}
ellipsizeMode="tail"
numberOfLines={1}
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}
@@ -242,17 +123,6 @@ exports[`TabBarBottom renders successfully 1`] = `
<View
navigation={
Object {
"addListener": [Function],
"dispatch": undefined,
"getParam": [Function],
"goBack": [Function],
"isFocused": [Function],
"navigate": [Function],
"pop": [Function],
"popToTop": [Function],
"push": [Function],
"replace": [Function],
"setParams": [Function],
"state": Object {
"key": "s1",
"routeName": "s1",

View File

@@ -1,76 +0,0 @@
import React from 'react';
import addNavigationHelpers from './addNavigationHelpers';
import getChildEventSubscriber from './getChildEventSubscriber';
/**
* HOC which caches the child navigation items.
*/
export default function withCachedChildNavigation(Comp) {
const displayName = Comp.displayName || Comp.name;
return class extends React.PureComponent {
static displayName = `withCachedChildNavigation(${displayName})`;
_childEventSubscribers = {};
componentWillMount() {
this._updateNavigationProps(this.props.navigation);
}
componentWillReceiveProps(nextProps) {
this._updateNavigationProps(nextProps.navigation);
}
componentDidUpdate() {
const activeKeys = this.props.navigation.state.routes.map(
route => route.key
);
Object.keys(this._childEventSubscribers).forEach(key => {
if (!activeKeys.includes(key)) {
delete this._childEventSubscribers[key];
}
});
}
_isRouteFocused = route => {
const { state } = this.props.navigation;
const focusedRoute = state.routes[state.index];
return route === focusedRoute;
};
_updateNavigationProps = navigation => {
// Update props for each child route
if (!this._childNavigationProps) {
this._childNavigationProps = {};
}
navigation.state.routes.forEach(route => {
const childNavigation = this._childNavigationProps[route.key];
if (childNavigation && childNavigation.state === route) {
return;
}
if (!this._childEventSubscribers[route.key]) {
this._childEventSubscribers[route.key] = getChildEventSubscriber(
navigation.addListener,
route.key
);
}
this._childNavigationProps[route.key] = addNavigationHelpers({
dispatch: navigation.dispatch,
state: route,
isFocused: () => this._isRouteFocused(route),
addListener: this._childEventSubscribers[route.key],
});
});
};
render() {
return (
<Comp
{...this.props}
childNavigationProps={this._childNavigationProps}
/>
);
}
};
}

View File

@@ -4440,9 +4440,9 @@ react-native-drawer-layout@1.3.2:
dependencies:
react-native-dismiss-keyboard "1.0.0"
react-native-safe-area-view@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/react-native-safe-area-view/-/react-native-safe-area-view-0.6.0.tgz#ce01eb27905a77780219537e0f53fe9c783a8b3d"
react-native-safe-area-view@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/react-native-safe-area-view/-/react-native-safe-area-view-0.7.0.tgz#38f5ab9368d6ef9e5d18ab64212938af3ec39421"
dependencies:
hoist-non-react-statics "^2.3.1"