mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-01-20 19:08:15 +08:00
Compare commits
21 Commits
v1.2.1
...
@ericvicen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ea01673e5 | ||
|
|
71adb7cc4f | ||
|
|
77343cb096 | ||
|
|
7c488c8d49 | ||
|
|
9005494e64 | ||
|
|
accee76951 | ||
|
|
0daab8c55b | ||
|
|
bbb8c4d8d3 | ||
|
|
ae98089337 | ||
|
|
57e37a8783 | ||
|
|
b7994d28db | ||
|
|
f1bfdeee46 | ||
|
|
c6301abaed | ||
|
|
4569ad49f9 | ||
|
|
214eeb13fb | ||
|
|
416fe58cab | ||
|
|
2e47cbb3cb | ||
|
|
cd99dc8054 | ||
|
|
e27ad22c57 | ||
|
|
6785729fb5 | ||
|
|
d3b6e70d16 |
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@@ -9,6 +9,8 @@ If you have a question, feature request, or an idea for improving the library or
|
||||
- [Get help on Discord chat (#react-navigation on Reactiflux)](https://discord.gg/4xEK3nD) or [on StackOverflow](https://stackoverflow.com/questions/tagged/react-navigation)
|
||||
- Search for your issue - it may have already been answered or even fixed in the development branch. However, if you find that an old, closed issue still persists in the latest version, you should open a new issue.
|
||||
|
||||
Bugs with react-navigation must be reproducible *without any external libraries that operate on it*. This means that if you are attempting to use Redux or MobX with it and you think you have found a bug, you must be able to reproduce it without Redux or MobX in this report. Redux related issues belong in [react-navigation-redux-helpers](https://github.com/react-navigation/react-navigation-redux-helpers), and we do not have any first-class integration with MobX at the moment.
|
||||
|
||||
---
|
||||
|
||||
### Current Behavior
|
||||
|
||||
13
CONTRIBUTING.md
Normal file
13
CONTRIBUTING.md
Normal 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!
|
||||
12
README.md
12
README.md
@@ -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?
|
||||
|
||||
|
||||
@@ -109,10 +109,10 @@ const ExampleInfo = {
|
||||
name: 'Animated Tabs Example',
|
||||
description: 'Tab transitions have custom animations',
|
||||
},
|
||||
// TabsWithNavigationFocus: {
|
||||
// name: 'withNavigationFocus',
|
||||
// description: 'Receive the focus prop to know when a screen is focused',
|
||||
// },
|
||||
TabsWithNavigationFocus: {
|
||||
name: 'withNavigationFocus',
|
||||
description: 'Receive the focus prop to know when a screen is focused',
|
||||
},
|
||||
};
|
||||
|
||||
const ExampleRoutes = {
|
||||
@@ -139,8 +139,8 @@ const ExampleRoutes = {
|
||||
screen: SimpleTabs,
|
||||
path: 'settings',
|
||||
},
|
||||
TabAnimations: TabAnimations,
|
||||
// TabsWithNavigationFocus: TabsWithNavigationFocus,
|
||||
TabAnimations,
|
||||
TabsWithNavigationFocus,
|
||||
};
|
||||
|
||||
type State = {
|
||||
@@ -305,7 +305,8 @@ const AppNavigator = StackNavigator(
|
||||
}
|
||||
);
|
||||
|
||||
export default () => <AppNavigator />;
|
||||
// export default () => <AppNavigator />;
|
||||
export default SimpleStack;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
item: {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -3,40 +3,75 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { SafeAreaView, Text } from 'react-native';
|
||||
import { Button, SafeAreaView, Text } from 'react-native';
|
||||
import { TabNavigator, withNavigationFocus } from 'react-navigation';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
|
||||
import SampleText from './SampleText';
|
||||
|
||||
const createTabScreen = (name, icon, focusedIcon, tintColor = '#673ab7') => {
|
||||
const TabScreen = ({ isFocused }) => (
|
||||
<SafeAreaView
|
||||
forceInset={{ horizontal: 'always', top: 'always' }}
|
||||
style={{
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontWeight: '700', fontSize: 16, marginBottom: 5 }}>
|
||||
{'Tab ' + name.toLowerCase()}
|
||||
class Child extends React.Component<any, any> {
|
||||
render() {
|
||||
return (
|
||||
<Text style={{ color: this.props.isFocused ? 'green' : 'maroon' }}>
|
||||
{this.props.isFocused
|
||||
? 'I know that my parent is focused!'
|
||||
: 'My parent is not focused! :O'}
|
||||
</Text>
|
||||
<Text>{'props.isFocused: ' + (isFocused ? ' true' : 'false')}</Text>
|
||||
</SafeAreaView>
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TabScreen.navigationOptions = {
|
||||
tabBarLabel: name,
|
||||
tabBarIcon: ({ tintColor, focused }) => (
|
||||
<MaterialCommunityIcons
|
||||
name={focused ? focusedIcon : icon}
|
||||
size={26}
|
||||
style={{ color: focused ? tintColor : '#ccc' }}
|
||||
/>
|
||||
),
|
||||
};
|
||||
const ChildWithNavigationFocus = withNavigationFocus(Child);
|
||||
|
||||
const createTabScreen = (name, icon, focusedIcon, tintColor = '#673ab7') => {
|
||||
class TabScreen extends React.Component<any, any> {
|
||||
static navigationOptions = {
|
||||
tabBarLabel: name,
|
||||
tabBarIcon: ({ tintColor, focused }) => (
|
||||
<MaterialCommunityIcons
|
||||
name={focused ? focusedIcon : icon}
|
||||
size={26}
|
||||
style={{ color: focused ? tintColor : '#ccc' }}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
state = { showChild: false };
|
||||
|
||||
render() {
|
||||
const { isFocused } = this.props;
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
forceInset={{ horizontal: 'always', top: 'always' }}
|
||||
style={{
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontWeight: '700', fontSize: 16, marginBottom: 5 }}>
|
||||
{'Tab ' + name.toLowerCase()}
|
||||
</Text>
|
||||
<Text style={{ marginBottom: 20 }}>
|
||||
{'props.isFocused: ' + (isFocused ? ' true' : 'false')}
|
||||
</Text>
|
||||
{this.state.showChild ? (
|
||||
<ChildWithNavigationFocus />
|
||||
) : (
|
||||
<Button
|
||||
title="Press me"
|
||||
onPress={() => this.setState({ showChild: true })}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
onPress={() => this.props.navigation.goBack(null)}
|
||||
title="Back to other examples"
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
return withNavigationFocus(TabScreen);
|
||||
};
|
||||
|
||||
|
||||
86
flow/react-navigation.js
vendored
86
flow/react-navigation.js
vendored
@@ -48,6 +48,15 @@ declare module 'react-navigation' {
|
||||
// react-native/Libraries/Animated/src/nodes/AnimatedValue.js
|
||||
declare type AnimatedValue = Object;
|
||||
|
||||
declare type HeaderForceInset = {
|
||||
horizontal?: string,
|
||||
vertical?: string,
|
||||
left?: string,
|
||||
right?: string,
|
||||
top?: string,
|
||||
bottom?: string,
|
||||
};
|
||||
|
||||
/**
|
||||
* Next, all the type declarations
|
||||
*/
|
||||
@@ -260,7 +269,8 @@ declare module 'react-navigation' {
|
||||
|
||||
declare export type NavigationComponent =
|
||||
| NavigationScreenComponent<NavigationRoute, *, *>
|
||||
| NavigationContainer<NavigationStateRoute, *, *>;
|
||||
| NavigationContainer<*, *, *>
|
||||
| any;
|
||||
|
||||
declare export type NavigationScreenComponent<
|
||||
Route: NavigationRoute,
|
||||
@@ -340,6 +350,7 @@ declare module 'react-navigation' {
|
||||
headerPressColorAndroid?: string,
|
||||
headerRight?: React$Node,
|
||||
headerStyle?: ViewStyleProp,
|
||||
headerForceInset?: HeaderForceInset,
|
||||
headerBackground?: React$Node | React$ElementType,
|
||||
gesturesEnabled?: boolean,
|
||||
gestureResponseDistance?: { vertical?: number, horizontal?: number },
|
||||
@@ -351,6 +362,7 @@ declare module 'react-navigation' {
|
||||
initialRouteParams?: NavigationParams,
|
||||
paths?: NavigationPathsConfig,
|
||||
navigationOptions?: NavigationScreenConfig<*>,
|
||||
initialRouteKey?: string,
|
||||
|};
|
||||
|
||||
declare export type NavigationStackViewConfig = {|
|
||||
@@ -447,7 +459,7 @@ declare module 'react-navigation' {
|
||||
type: EventType,
|
||||
action: NavigationAction,
|
||||
state: NavigationState,
|
||||
lastState: NavigationState,
|
||||
lastState: ?NavigationState,
|
||||
};
|
||||
|
||||
declare export type NavigationEventCallback = (
|
||||
@@ -492,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
|
||||
*/
|
||||
@@ -689,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',
|
||||
@@ -701,7 +686,7 @@ declare module 'react-navigation' {
|
||||
SET_PARAMS: 'Navigation/SET_PARAMS',
|
||||
URI: 'Navigation/URI',
|
||||
back: {
|
||||
(payload: { key?: ?string }): NavigationBackAction,
|
||||
(payload?: { key?: ?string }): NavigationBackAction,
|
||||
toString: () => string,
|
||||
},
|
||||
init: {
|
||||
@@ -740,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,
|
||||
@@ -791,9 +777,6 @@ declare module 'react-navigation' {
|
||||
drawerLockMode?: 'unlocked' | 'locked-closed' | 'locked-open',
|
||||
drawerWidth?: number | (() => number),
|
||||
drawerPosition?: 'left' | 'right',
|
||||
drawerOpenRoute?: string,
|
||||
drawerCloseRoute?: string,
|
||||
drawerToggleRoute?: string,
|
||||
contentComponent?: React$ElementType,
|
||||
contentOptions?: {},
|
||||
style?: ViewStyleProp,
|
||||
@@ -926,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,
|
||||
@@ -965,6 +945,8 @@ declare module 'react-navigation' {
|
||||
itemsContainerStyle?: ViewStyleProp,
|
||||
itemStyle?: ViewStyleProp,
|
||||
labelStyle?: TextStyleProp,
|
||||
activeLabelStyle?: TextStyleProp,
|
||||
inactiveLabelStyle?: TextStyleProp,
|
||||
iconContainerStyle?: ViewStyleProp,
|
||||
drawerPosition: 'left' | 'right',
|
||||
};
|
||||
@@ -1045,7 +1027,7 @@ declare module 'react-navigation' {
|
||||
declare export var TabBarBottom: React$ComponentType<_TabBarBottomProps>;
|
||||
|
||||
declare type _NavigationInjectedProps = {
|
||||
navigation: NavigationScreenProp<NavigationState>,
|
||||
navigation: NavigationScreenProp<NavigationStateRoute>,
|
||||
};
|
||||
declare export function withNavigation<T: {}>(
|
||||
Component: React$ComponentType<T & _NavigationInjectedProps>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 />;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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()),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,6 +11,18 @@ export default function getChildEventSubscriber(addListener, key) {
|
||||
const willBlurSubscribers = new Set();
|
||||
const didBlurSubscribers = new Set();
|
||||
|
||||
const removeAll = () => {
|
||||
[
|
||||
actionSubscribers,
|
||||
willFocusSubscribers,
|
||||
didFocusSubscribers,
|
||||
willBlurSubscribers,
|
||||
didBlurSubscribers,
|
||||
].forEach(set => set.clear());
|
||||
|
||||
upstreamSubscribers.forEach(subs => subs && subs.remove());
|
||||
};
|
||||
|
||||
const getChildSubscribers = evtName => {
|
||||
switch (evtName) {
|
||||
case 'action':
|
||||
@@ -43,10 +55,6 @@ export default function getChildEventSubscriber(addListener, key) {
|
||||
// considered blurred
|
||||
let lastEmittedEvent = 'didBlur';
|
||||
|
||||
const cleanup = () => {
|
||||
upstreamSubscribers.forEach(subs => subs && subs.remove());
|
||||
};
|
||||
|
||||
const upstreamEvents = [
|
||||
'willFocus',
|
||||
'didFocus',
|
||||
@@ -76,7 +84,7 @@ export default function getChildEventSubscriber(addListener, key) {
|
||||
action,
|
||||
type: eventName,
|
||||
};
|
||||
const isTransitioning = !!state && state.isTransitioning;
|
||||
const isTransitioning = !!state && !!state.transitioningFromKey;
|
||||
|
||||
const previouslyLastEmittedEvent = lastEmittedEvent;
|
||||
|
||||
@@ -133,15 +141,18 @@ export default function getChildEventSubscriber(addListener, key) {
|
||||
})
|
||||
);
|
||||
|
||||
return (eventName, eventHandler) => {
|
||||
const subscribers = getChildSubscribers(eventName);
|
||||
if (!subscribers) {
|
||||
throw new Error(`Invalid event name "${eventName}"`);
|
||||
}
|
||||
subscribers.add(eventHandler);
|
||||
const remove = () => {
|
||||
subscribers.delete(eventHandler);
|
||||
};
|
||||
return { remove };
|
||||
return {
|
||||
removeAll,
|
||||
addListener(eventName, eventHandler) {
|
||||
const subscribers = getChildSubscribers(eventName);
|
||||
if (!subscribers) {
|
||||
throw new Error(`Invalid event name "${eventName}"`);
|
||||
}
|
||||
subscribers.add(eventHandler);
|
||||
const remove = () => {
|
||||
subscribers.delete(eventHandler);
|
||||
};
|
||||
return { remove };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import React from 'react';
|
||||
import createNavigationContainer from '../createNavigationContainer';
|
||||
import createNavigator from './createNavigator';
|
||||
import CardStackTransitioner from '../views/CardStack/CardStackTransitioner';
|
||||
import StackRouter from '../routers/StackRouter';
|
||||
import NavigationActions from '../NavigationActions';
|
||||
|
||||
// A stack navigators props are the intersection between
|
||||
// the base navigator props (navgiation, screenProps, etc)
|
||||
// and the view's props
|
||||
|
||||
export default (routeConfigMap, stackConfig = {}) => {
|
||||
const {
|
||||
initialRouteName,
|
||||
initialRouteParams,
|
||||
paths,
|
||||
headerMode,
|
||||
headerTransitionPreset,
|
||||
mode,
|
||||
cardStyle,
|
||||
transitionConfig,
|
||||
onTransitionStart,
|
||||
onTransitionEnd,
|
||||
navigationOptions,
|
||||
} = stackConfig;
|
||||
|
||||
const stackRouterConfig = {
|
||||
initialRouteName,
|
||||
initialRouteParams,
|
||||
paths,
|
||||
navigationOptions,
|
||||
};
|
||||
|
||||
const router = StackRouter(routeConfigMap, stackRouterConfig);
|
||||
|
||||
// Create a navigator with CardStackTransitioner as the view
|
||||
const navigator = createNavigator(router, routeConfigMap, stackConfig)(
|
||||
props => (
|
||||
<CardStackTransitioner
|
||||
{...props}
|
||||
headerMode={headerMode}
|
||||
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();
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
return createNavigationContainer(navigator);
|
||||
};
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 }) => ({
|
||||
|
||||
@@ -224,6 +224,7 @@ exports[`DrawerNavigator renders successfully 1`] = `
|
||||
"color": "#2196f3",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
|
||||
@@ -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,
|
||||
@@ -80,10 +80,11 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#F7F7F7",
|
||||
"backgroundColor": "red",
|
||||
"borderBottomColor": "#A7A7AA",
|
||||
"borderBottomWidth": 0.5,
|
||||
"height": 64,
|
||||
"opacity": 0.5,
|
||||
"paddingBottom": 0,
|
||||
"paddingLeft": 0,
|
||||
"paddingRight": 0,
|
||||
@@ -233,7 +234,7 @@ exports[`StackNavigator renders successfully 1`] = `
|
||||
pointerEvents="auto"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#EFEFF4",
|
||||
"backgroundColor": "#E9E9EF",
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"opacity": 1,
|
||||
@@ -265,10 +266,11 @@ exports[`StackNavigator renders successfully 1`] = `
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#F7F7F7",
|
||||
"backgroundColor": "red",
|
||||
"borderBottomColor": "#A7A7AA",
|
||||
"borderBottomWidth": 0.5,
|
||||
"height": 64,
|
||||
"opacity": 0.5,
|
||||
"paddingBottom": 0,
|
||||
"paddingLeft": 0,
|
||||
"paddingRight": 0,
|
||||
|
||||
@@ -147,13 +147,12 @@ exports[`TabNavigator renders successfully 1`] = `
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"bottom": 0,
|
||||
"alignSelf": "center",
|
||||
"height": "100%",
|
||||
"justifyContent": "center",
|
||||
"left": 0,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
/>
|
||||
@@ -162,13 +161,12 @@ exports[`TabNavigator renders successfully 1`] = `
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"bottom": 0,
|
||||
"alignSelf": "center",
|
||||
"height": "100%",
|
||||
"justifyContent": "center",
|
||||
"left": 0,
|
||||
"opacity": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
|
||||
31
src/navigators/createStackNavigator.js
Normal file
31
src/navigators/createStackNavigator.js
Normal 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;
|
||||
@@ -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);
|
||||
};
|
||||
15
src/react-navigation.js
vendored
15
src/react-navigation.js
vendored
@@ -20,10 +20,10 @@ module.exports = {
|
||||
return require('./navigators/createNavigator').default;
|
||||
},
|
||||
get StackNavigator() {
|
||||
return require('./navigators/StackNavigator').default;
|
||||
return require('./navigators/createStackNavigator').default;
|
||||
},
|
||||
get TabNavigator() {
|
||||
return require('./navigators/TabNavigator').default;
|
||||
return require('./navigators/createTabNavigator').default;
|
||||
},
|
||||
get DrawerNavigator() {
|
||||
return require('./navigators/DrawerNavigator').default;
|
||||
@@ -41,14 +41,11 @@ module.exports = {
|
||||
get Transitioner() {
|
||||
return require('./views/Transitioner').default;
|
||||
},
|
||||
get CardStackTransitioner() {
|
||||
return require('./views/CardStack/CardStackTransitioner').default;
|
||||
get StackView() {
|
||||
return require('./views/StackView/StackView').default;
|
||||
},
|
||||
get CardStack() {
|
||||
return require('./views/CardStack/CardStack').default;
|
||||
},
|
||||
get Card() {
|
||||
return require('./views/CardStack/Card').default;
|
||||
get StackViewCard() {
|
||||
return require('./views/StackView/StackViewCard').default;
|
||||
},
|
||||
get SafeAreaView() {
|
||||
return require('react-native-safe-area-view').default;
|
||||
|
||||
55
src/routers/DrawerRouter.js
Normal file
55
src/routers/DrawerRouter.js
Normal 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;
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -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: [
|
||||
{
|
||||
@@ -92,15 +91,16 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
...(action.params || {}),
|
||||
...(initialRouteParams || {}),
|
||||
};
|
||||
const { initialRouteKey } = stackConfig;
|
||||
route = {
|
||||
...route,
|
||||
...(params ? { params } : {}),
|
||||
routeName: initialRouteName,
|
||||
key: action.key || generateKey(),
|
||||
key: action.key || (initialRouteKey || generateKey()),
|
||||
};
|
||||
return {
|
||||
key: 'StackRouterRoot',
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: false,
|
||||
index: 0,
|
||||
routes: [route],
|
||||
};
|
||||
@@ -157,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
|
||||
@@ -221,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,
|
||||
};
|
||||
@@ -253,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 &&
|
||||
@@ -320,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]],
|
||||
};
|
||||
@@ -357,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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -440,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 &&
|
||||
@@ -576,7 +577,5 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
routeConfigs,
|
||||
stackConfig.navigationOptions
|
||||
),
|
||||
|
||||
getScreenConfig: getScreenConfigDeprecated,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
72
src/routers/__tests__/DrawerRouter-test.js
Normal file
72
src/routers/__tests__/DrawerRouter-test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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', () => {
|
||||
@@ -576,6 +576,23 @@ describe('StackRouter', () => {
|
||||
expect(state2.routes[1].routes[1].routes[1].routeName).toEqual('Corge');
|
||||
});
|
||||
|
||||
test('Navigate to initial screen is possible', () => {
|
||||
const TestRouter = StackRouter(
|
||||
{
|
||||
foo: { screen: () => <div /> },
|
||||
bar: { screen: () => <div /> },
|
||||
},
|
||||
{ initialRouteKey: 'foo' }
|
||||
);
|
||||
const initState = TestRouter.getStateForAction(NavigationActions.init());
|
||||
const pushedState = TestRouter.getStateForAction(
|
||||
NavigationActions.navigate({ routeName: 'foo', key: 'foo' }),
|
||||
initState
|
||||
);
|
||||
expect(pushedState.index).toEqual(0);
|
||||
expect(pushedState.routes[0].routeName).toEqual('foo');
|
||||
});
|
||||
|
||||
test('Navigate with key is idempotent', () => {
|
||||
const TestRouter = StackRouter({
|
||||
foo: { screen: () => <div /> },
|
||||
@@ -661,7 +678,7 @@ describe('StackRouter', () => {
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
expect(state).toEqual({
|
||||
index: 0,
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: null,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
@@ -689,7 +706,7 @@ describe('StackRouter', () => {
|
||||
);
|
||||
expect(state3).toEqual({
|
||||
index: 0,
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: null,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
@@ -756,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,
|
||||
@@ -764,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', () => {
|
||||
@@ -786,7 +803,7 @@ describe('StackRouter', () => {
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
expect(state).toEqual({
|
||||
index: 0,
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: null,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
@@ -814,7 +831,7 @@ describe('StackRouter', () => {
|
||||
);
|
||||
expect(state3).toEqual({
|
||||
index: 0,
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: null,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
@@ -888,7 +905,7 @@ describe('StackRouter', () => {
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
expect(state).toEqual({
|
||||
index: 0,
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: null,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
@@ -912,7 +929,7 @@ describe('StackRouter', () => {
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
expect(state).toEqual({
|
||||
index: 0,
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: null,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
@@ -1257,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',
|
||||
@@ -1316,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',
|
||||
@@ -1431,7 +1448,7 @@ describe('StackRouter', () => {
|
||||
|
||||
const state = {
|
||||
index: 0,
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: null,
|
||||
routes: [
|
||||
{
|
||||
index: 1,
|
||||
@@ -1647,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(
|
||||
{
|
||||
@@ -1658,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);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`validateRouteConfigMap Fails if both screen and getScreen are defined 1`] = `"Route 'Home' should declare a screen or a getScreen, not both."`;
|
||||
|
||||
exports[`validateRouteConfigMap Fails on bad object 1`] = `
|
||||
"The component for route 'Home' must be a React component. For example:
|
||||
|
||||
import MyScreen from './MyScreen';
|
||||
...
|
||||
Home: MyScreen,
|
||||
}
|
||||
|
||||
You can also use a navigator:
|
||||
|
||||
import MyNavigator from './MyNavigator';
|
||||
...
|
||||
Home: MyNavigator,
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`validateRouteConfigMap Fails on empty bare screen 1`] = `
|
||||
"The component for route 'Home' must be a React component. For example:
|
||||
|
||||
import MyScreen from './MyScreen';
|
||||
...
|
||||
Home: MyScreen,
|
||||
}
|
||||
|
||||
You can also use a navigator:
|
||||
|
||||
import MyNavigator from './MyNavigator';
|
||||
...
|
||||
Home: MyNavigator,
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`validateRouteConfigMap Fails on empty config 1`] = `"Please specify at least one route when configuring a navigator."`;
|
||||
@@ -13,9 +13,19 @@ ProfileNavigator.router = StackRouter({
|
||||
});
|
||||
|
||||
describe('validateRouteConfigMap', () => {
|
||||
test('Fails on empty bare screen', () => {
|
||||
const invalidMap = {
|
||||
Home: undefined,
|
||||
};
|
||||
expect(() =>
|
||||
validateRouteConfigMap(invalidMap)
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
test('Fails on empty config', () => {
|
||||
const invalidMap = {};
|
||||
expect(() => validateRouteConfigMap(invalidMap)).toThrow();
|
||||
expect(() =>
|
||||
validateRouteConfigMap(invalidMap)
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
test('Fails on bad object', () => {
|
||||
const invalidMap = {
|
||||
@@ -23,7 +33,9 @@ describe('validateRouteConfigMap', () => {
|
||||
foo: 'bar',
|
||||
},
|
||||
};
|
||||
expect(() => validateRouteConfigMap(invalidMap)).toThrow();
|
||||
expect(() =>
|
||||
validateRouteConfigMap(invalidMap)
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
test('Fails if both screen and getScreen are defined', () => {
|
||||
const invalidMap = {
|
||||
@@ -32,15 +44,17 @@ describe('validateRouteConfigMap', () => {
|
||||
getScreen: () => ListScreen,
|
||||
},
|
||||
};
|
||||
expect(() => validateRouteConfigMap(invalidMap)).toThrow();
|
||||
expect(() =>
|
||||
validateRouteConfigMap(invalidMap)
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
test('Succeeds on a valid config', () => {
|
||||
const invalidMap = {
|
||||
const validMap = {
|
||||
Home: {
|
||||
screen: ProfileNavigator,
|
||||
},
|
||||
Chat: ListScreen,
|
||||
};
|
||||
validateRouteConfigMap(invalidMap);
|
||||
validateRouteConfigMap(validMap);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import invariant from '../utils/invariant';
|
||||
|
||||
export default () =>
|
||||
invariant(
|
||||
false,
|
||||
'`getScreenConfig` has been replaced with `getScreenOptions`'
|
||||
);
|
||||
@@ -13,16 +13,13 @@ function validateRouteConfigMap(routeConfigs) {
|
||||
|
||||
routeNames.forEach(routeName => {
|
||||
const routeConfig = routeConfigs[routeName];
|
||||
|
||||
const screenComponent = routeConfig.screen
|
||||
? routeConfig.screen
|
||||
: routeConfig;
|
||||
const screenComponent = getScreenComponent(routeConfig);
|
||||
|
||||
if (
|
||||
screenComponent &&
|
||||
typeof screenComponent !== 'function' &&
|
||||
typeof screenComponent !== 'string' &&
|
||||
!routeConfig.getScreen
|
||||
!screenComponent ||
|
||||
(typeof screenComponent !== 'function' &&
|
||||
typeof screenComponent !== 'string' &&
|
||||
!routeConfig.getScreen)
|
||||
) {
|
||||
throw new Error(
|
||||
`The component for route '${routeName}' must be a ` +
|
||||
@@ -48,4 +45,12 @@ function validateRouteConfigMap(routeConfigs) {
|
||||
});
|
||||
}
|
||||
|
||||
function getScreenComponent(routeConfig) {
|
||||
if (!routeConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return routeConfig.screen ? routeConfig.screen : routeConfig;
|
||||
}
|
||||
|
||||
export default validateRouteConfigMap;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -21,6 +21,8 @@ const DrawerNavigatorItems = ({
|
||||
itemsContainerStyle,
|
||||
itemStyle,
|
||||
labelStyle,
|
||||
activeLabelStyle,
|
||||
inactiveLabelStyle,
|
||||
iconContainerStyle,
|
||||
drawerPosition,
|
||||
}) => (
|
||||
@@ -34,6 +36,7 @@ const DrawerNavigatorItems = ({
|
||||
const scene = { route, index, focused, tintColor: color };
|
||||
const icon = renderIcon(scene);
|
||||
const label = getLabel(scene);
|
||||
const extraLabelStyle = focused ? activeLabelStyle : inactiveLabelStyle;
|
||||
return (
|
||||
<TouchableItem
|
||||
key={route.key}
|
||||
@@ -63,7 +66,9 @@ const DrawerNavigatorItems = ({
|
||||
</View>
|
||||
) : null}
|
||||
{typeof label === 'string' ? (
|
||||
<Text style={[styles.label, { color }, labelStyle]}>
|
||||
<Text
|
||||
style={[styles.label, { color }, labelStyle, extraLabelStyle]}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
) : (
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -4,7 +4,7 @@ import DrawerLayout from 'react-native-drawer-layout-polyfill';
|
||||
|
||||
import addNavigationHelpers from '../../addNavigationHelpers';
|
||||
import DrawerSidebar from './DrawerSidebar';
|
||||
import getChildEventSubscriber from '../../getChildEventSubscriber';
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
|
||||
/**
|
||||
* Component that renders the drawer.
|
||||
@@ -12,14 +12,12 @@ import getChildEventSubscriber from '../../getChildEventSubscriber';
|
||||
export default class DrawerView extends React.PureComponent {
|
||||
state = {
|
||||
drawerWidth:
|
||||
typeof this.props.drawerWidth === 'function'
|
||||
? this.props.drawerWidth()
|
||||
: this.props.drawerWidth,
|
||||
typeof this.props.navigationConfig.drawerWidth === 'function'
|
||||
? this.props.navigationConfig.drawerWidth()
|
||||
: this.props.navigationConfig.drawerWidth,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
this._updateScreenNavigation(this.props.navigation);
|
||||
|
||||
Dimensions.addEventListener('change', this._updateWidth);
|
||||
}
|
||||
|
||||
@@ -28,107 +26,59 @@ export default class DrawerView extends React.PureComponent {
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (
|
||||
this.props.navigation.state.index !== nextProps.navigation.state.index
|
||||
) {
|
||||
const {
|
||||
drawerOpenRoute,
|
||||
drawerCloseRoute,
|
||||
drawerToggleRoute,
|
||||
} = this.props;
|
||||
const { routes, index } = nextProps.navigation.state;
|
||||
if (routes[index].routeName === drawerOpenRoute) {
|
||||
this._drawer.openDrawer();
|
||||
} else if (routes[index].routeName === drawerToggleRoute) {
|
||||
if (this.props.navigation.state.index === 0) {
|
||||
this.props.navigation.navigate(drawerOpenRoute);
|
||||
} else {
|
||||
this.props.navigation.navigate(drawerCloseRoute);
|
||||
}
|
||||
} else {
|
||||
this._drawer.closeDrawer();
|
||||
}
|
||||
const { isDrawerOpen } = nextProps.navigation.state;
|
||||
const wasDrawerOpen = this.props.navigation.state.isDrawerOpen;
|
||||
if (isDrawerOpen && !wasDrawerOpen) {
|
||||
this._drawer.openDrawer();
|
||||
} else if (wasDrawerOpen && !isDrawerOpen) {
|
||||
this._drawer.closeDrawer();
|
||||
}
|
||||
this._updateScreenNavigation(nextProps.navigation);
|
||||
}
|
||||
|
||||
_handleDrawerOpen = () => {
|
||||
const { navigation, drawerOpenRoute } = this.props;
|
||||
const { routes, index } = navigation.state;
|
||||
if (routes[index].routeName !== drawerOpenRoute) {
|
||||
this.props.navigation.navigate(drawerOpenRoute);
|
||||
}
|
||||
const { navigation } = this.props;
|
||||
navigation.dispatch({ type: NavigationActions.OPEN_DRAWER });
|
||||
};
|
||||
|
||||
_handleDrawerClose = () => {
|
||||
const { navigation, drawerCloseRoute } = this.props;
|
||||
const { routes, index } = navigation.state;
|
||||
if (routes[index].routeName !== drawerCloseRoute) {
|
||||
this.props.navigation.navigate(drawerCloseRoute);
|
||||
}
|
||||
};
|
||||
|
||||
_updateScreenNavigation = navigation => {
|
||||
const { drawerCloseRoute } = this.props;
|
||||
const navigationState = navigation.state.routes.find(
|
||||
route => route.routeName === drawerCloseRoute
|
||||
);
|
||||
if (
|
||||
this._screenNavigationProp &&
|
||||
this._screenNavigationProp.state === navigationState
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._screenNavigationProp = addNavigationHelpers({
|
||||
dispatch: navigation.dispatch,
|
||||
state: navigationState,
|
||||
addListener: getChildEventSubscriber(
|
||||
navigation.addListener,
|
||||
navigationState.key
|
||||
),
|
||||
});
|
||||
const { navigation } = this.props;
|
||||
navigation.dispatch({ type: NavigationActions.CLOSE_DRAWER });
|
||||
};
|
||||
|
||||
_updateWidth = () => {
|
||||
const drawerWidth =
|
||||
typeof this.props.drawerWidth === 'function'
|
||||
? this.props.drawerWidth()
|
||||
: this.props.drawerWidth;
|
||||
typeof this.props.navigationConfig.drawerWidth === 'function'
|
||||
? this.props.navigationConfig.drawerWidth()
|
||||
: this.props.navigationConfig.drawerWidth;
|
||||
|
||||
if (this.state.drawerWidth !== drawerWidth) {
|
||||
this.setState({ drawerWidth });
|
||||
}
|
||||
};
|
||||
|
||||
_getNavigationState = navigation => {
|
||||
const { drawerCloseRoute } = this.props;
|
||||
const navigationState = navigation.state.routes.find(
|
||||
route => route.routeName === drawerCloseRoute
|
||||
_renderNavigationView = () => {
|
||||
return (
|
||||
<DrawerSidebar
|
||||
screenProps={this.props.screenProps}
|
||||
navigation={this.props.navigation}
|
||||
descriptors={this.props.descriptors}
|
||||
contentComponent={this.props.navigationConfig.contentComponent}
|
||||
contentOptions={this.props.navigationConfig.contentOptions}
|
||||
drawerPosition={this.props.navigationConfig.drawerPosition}
|
||||
style={this.props.navigationConfig.style}
|
||||
{...this.props.navigationConfig}
|
||||
/>
|
||||
);
|
||||
return navigationState;
|
||||
};
|
||||
|
||||
_renderNavigationView = () => (
|
||||
<DrawerSidebar
|
||||
screenProps={this.props.screenProps}
|
||||
navigation={this._screenNavigationProp}
|
||||
router={this.props.router}
|
||||
contentComponent={this.props.contentComponent}
|
||||
contentOptions={this.props.contentOptions}
|
||||
drawerPosition={this.props.drawerPosition}
|
||||
style={this.props.style}
|
||||
/>
|
||||
);
|
||||
|
||||
render() {
|
||||
const DrawerScreen = this.props.router.getComponentForRouteName(
|
||||
this.props.drawerCloseRoute
|
||||
);
|
||||
const { state } = this.props.navigation;
|
||||
const activeKey = state.routes[state.index].key;
|
||||
const descriptor = this.props.descriptors[activeKey];
|
||||
|
||||
const config = this.props.router.getScreenOptions(
|
||||
this._screenNavigationProp,
|
||||
this.props.screenProps
|
||||
);
|
||||
const DrawerScreen = descriptor.getComponent();
|
||||
|
||||
const { drawerLockMode } = descriptor.options;
|
||||
|
||||
return (
|
||||
<DrawerLayout
|
||||
@@ -137,23 +87,25 @@ export default class DrawerView extends React.PureComponent {
|
||||
}}
|
||||
drawerLockMode={
|
||||
(this.props.screenProps && this.props.screenProps.drawerLockMode) ||
|
||||
(config && config.drawerLockMode)
|
||||
this.props.navigationConfig.drawerLockMode
|
||||
}
|
||||
drawerBackgroundColor={
|
||||
this.props.navigationConfig.drawerBackgroundColor
|
||||
}
|
||||
drawerBackgroundColor={this.props.drawerBackgroundColor}
|
||||
drawerWidth={this.state.drawerWidth}
|
||||
onDrawerOpen={this._handleDrawerOpen}
|
||||
onDrawerClose={this._handleDrawerClose}
|
||||
useNativeAnimations={this.props.useNativeAnimations}
|
||||
useNativeAnimations={this.props.navigationConfig.useNativeAnimations}
|
||||
renderNavigationView={this._renderNavigationView}
|
||||
drawerPosition={
|
||||
this.props.drawerPosition === 'right'
|
||||
this.props.navigationConfig.drawerPosition === 'right'
|
||||
? DrawerLayout.positions.Right
|
||||
: DrawerLayout.positions.Left
|
||||
}
|
||||
>
|
||||
<DrawerScreen
|
||||
screenProps={this.props.screenProps}
|
||||
navigation={this._screenNavigationProp}
|
||||
navigation={descriptor.navigation}
|
||||
/>
|
||||
</DrawerLayout>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
@@ -476,11 +473,11 @@ class Header extends React.PureComponent {
|
||||
safeHeaderStyle,
|
||||
];
|
||||
|
||||
const { headerForceInset } = options;
|
||||
const forceInset = headerForceInset || { top: 'always', bottom: 'never' };
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
forceInset={{ top: 'always', bottom: 'never' }}
|
||||
style={containerStyles}
|
||||
>
|
||||
<SafeAreaView forceInset={forceInset} style={containerStyles}>
|
||||
<View style={StyleSheet.absoluteFill}>{options.headerBackground}</View>
|
||||
<View style={{ flex: 1 }}>{appBar}</View>
|
||||
</SafeAreaView>
|
||||
|
||||
594
src/views/Header/Header2.js
Normal file
594
src/views/Header/Header2.js
Normal 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);
|
||||
176
src/views/Header/HeaderStyleInterpolator2.js
Normal file
176
src/views/Header/HeaderStyleInterpolator2.js
Normal 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,
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
66
src/views/StackView/StackView.js
Normal file
66
src/views/StackView/StackView.js
Normal 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;
|
||||
546
src/views/StackView/StackView2.js
Normal file
546
src/views/StackView/StackView2.js
Normal 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;
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,52 +78,15 @@ class CardStack extends React.Component {
|
||||
*/
|
||||
_immediateIndex = null;
|
||||
|
||||
_screenDetails = {};
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_getScreenDetails = scene => {
|
||||
const { screenProps, transitionProps: { navigation }, router } = this.props;
|
||||
let screenDetails = this._screenDetails[scene.key];
|
||||
if (!screenDetails || screenDetails.state !== scene.route) {
|
||||
const screenNavigation = addNavigationHelpers({
|
||||
dispatch: navigation.dispatch,
|
||||
state: scene.route,
|
||||
addListener: getChildEventSubscriber(
|
||||
navigation.addListener,
|
||||
scene.route.key
|
||||
),
|
||||
});
|
||||
screenDetails = {
|
||||
state: scene.route,
|
||||
navigation: screenNavigation,
|
||||
options: router.getScreenOptions(screenNavigation, screenProps),
|
||||
};
|
||||
this._screenDetails[scene.key] = screenDetails;
|
||||
}
|
||||
return screenDetails;
|
||||
};
|
||||
|
||||
_renderHeader(scene, headerMode) {
|
||||
const { header } = this._getScreenDetails(scene).options;
|
||||
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,
|
||||
@@ -145,7 +106,6 @@ class CardStack extends React.Component {
|
||||
scene,
|
||||
mode: headerMode,
|
||||
transitionPreset: this._getHeaderTransitionPreset(),
|
||||
getScreenDetails: this._getScreenDetails,
|
||||
leftInterpolator: headerLeftInterpolator,
|
||||
titleInterpolator: headerTitleInterpolator,
|
||||
rightInterpolator: headerRightInterpolator,
|
||||
@@ -245,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 =
|
||||
@@ -261,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;
|
||||
});
|
||||
@@ -285,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
|
||||
@@ -388,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>
|
||||
@@ -420,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') {
|
||||
@@ -440,7 +406,7 @@ class CardStack extends React.Component {
|
||||
}
|
||||
return (
|
||||
<SceneView
|
||||
screenProps={this.props.screenProps}
|
||||
screenProps={screenProps}
|
||||
navigation={navigation}
|
||||
component={SceneComponent}
|
||||
/>
|
||||
@@ -464,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>
|
||||
);
|
||||
};
|
||||
@@ -498,4 +457,4 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
export default CardStack;
|
||||
export default StackViewLayout;
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
268
src/views/StackView/StackViewTransitions.js
Normal file
268
src/views/StackView/StackViewTransitions.js
Normal 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,
|
||||
};
|
||||
225
src/views/StackView/Transitioner2.js
Normal file
225
src/views/StackView/Transitioner2.js
Normal 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;
|
||||
42
src/views/StackView/createPointerEventsContainer.js
Normal file
42
src/views/StackView/createPointerEventsContainer.js
Normal 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;
|
||||
}
|
||||
@@ -53,12 +53,11 @@ const styles = StyleSheet.create({
|
||||
// We render the icon twice at the same position on top of each other:
|
||||
// active and inactive one, so we can fade between them:
|
||||
// Cover the whole iconContainer:
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
alignItems: 'center',
|
||||
alignSelf: 'center',
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -20,127 +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",
|
||||
"bottom": 0,
|
||||
"justifyContent": "center",
|
||||
"left": 0,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"bottom": 0,
|
||||
"justifyContent": "center",
|
||||
"left": 0,
|
||||
"opacity": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
accessible={true}
|
||||
allowFontScaling={true}
|
||||
collapsable={undefined}
|
||||
ellipsizeMode="tail"
|
||||
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}
|
||||
@@ -244,16 +123,6 @@ exports[`TabBarBottom renders successfully 1`] = `
|
||||
<View
|
||||
navigation={
|
||||
Object {
|
||||
"addListener": [Function],
|
||||
"dispatch": undefined,
|
||||
"getParam": [Function],
|
||||
"goBack": [Function],
|
||||
"navigate": [Function],
|
||||
"pop": [Function],
|
||||
"popToTop": [Function],
|
||||
"push": [Function],
|
||||
"replace": [Function],
|
||||
"setParams": [Function],
|
||||
"state": Object {
|
||||
"key": "s1",
|
||||
"routeName": "s1",
|
||||
|
||||
@@ -12,9 +12,13 @@ export default function withNavigationFocus(Component) {
|
||||
navigation: propTypes.object.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
isFocused: false,
|
||||
};
|
||||
constructor(props, context) {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
isFocused: this.getNavigation(props, context).isFocused(),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const navigation = this.getNavigation();
|
||||
@@ -32,8 +36,8 @@ export default function withNavigationFocus(Component) {
|
||||
this.subscriptions.forEach(sub => sub.remove());
|
||||
}
|
||||
|
||||
getNavigation = () => {
|
||||
const navigation = this.props.navigation || this.context.navigation;
|
||||
getNavigation = (props = this.props, context = this.context) => {
|
||||
const navigation = props.navigation || context.navigation;
|
||||
invariant(
|
||||
!!navigation,
|
||||
'withNavigationFocus can only be used on a view hierarchy of a navigator. The wrapped component is unable to get access to navigation from props or context.'
|
||||
|
||||
@@ -1,51 +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})`;
|
||||
|
||||
componentWillMount() {
|
||||
this._updateNavigationProps(this.props.navigation);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this._updateNavigationProps(nextProps.navigation);
|
||||
}
|
||||
|
||||
_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;
|
||||
}
|
||||
this._childNavigationProps[route.key] = addNavigationHelpers({
|
||||
dispatch: navigation.dispatch,
|
||||
state: route,
|
||||
addListener: getChildEventSubscriber(
|
||||
navigation.addListener,
|
||||
route.key
|
||||
),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Comp
|
||||
{...this.props}
|
||||
childNavigationProps={this._childNavigationProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user