mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-01-21 03:18:18 +08:00
Compare commits
51 Commits
@ericvicen
...
v1.5.10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3eeea48fa3 | ||
|
|
3039ce1a5f | ||
|
|
cbe46d7303 | ||
|
|
11a99c8aaf | ||
|
|
6b5100c335 | ||
|
|
34dd1fe490 | ||
|
|
0cc9070dcd | ||
|
|
e369c89b81 | ||
|
|
99bf123ff2 | ||
|
|
feb93411bc | ||
|
|
86f07175fb | ||
|
|
8288853e3c | ||
|
|
c4bd2db542 | ||
|
|
ee40dd7d24 | ||
|
|
18a48105c2 | ||
|
|
fbac47b696 | ||
|
|
9aab47dac2 | ||
|
|
67309c00a6 | ||
|
|
86a724cfe3 | ||
|
|
eb78128439 | ||
|
|
c39ec7a10c | ||
|
|
0ff3347e97 | ||
|
|
b9d55a6330 | ||
|
|
315e43701b | ||
|
|
1d573bc246 | ||
|
|
3bfb0b90d0 | ||
|
|
8a129afe13 | ||
|
|
ab2a63fe92 | ||
|
|
c411210ecc | ||
|
|
01e7296520 | ||
|
|
8f3e0997c5 | ||
|
|
3f3ef6485c | ||
|
|
b12abb553f | ||
|
|
e02841a979 | ||
|
|
e147f34555 | ||
|
|
81e0ce136e | ||
|
|
8ba727c2cf | ||
|
|
9a86ef8362 | ||
|
|
4fe7c92847 | ||
|
|
afecaaed7f | ||
|
|
6373b802dd | ||
|
|
138151433d | ||
|
|
2744cb32b7 | ||
|
|
439b4222ce | ||
|
|
ba0b1861e5 | ||
|
|
318788ca60 | ||
|
|
498a39c200 | ||
|
|
b31ebef5b0 | ||
|
|
f4fe588e08 | ||
|
|
403af82c3f | ||
|
|
0c2360dc36 |
@@ -1,13 +0,0 @@
|
||||
# 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,7 +39,17 @@ See [the help page](https://reactnavigation.org/en/help.html).
|
||||
|
||||
#### How can I help?
|
||||
|
||||
See our [Contributing Guide](CONTRIBUTING.md)!
|
||||
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!
|
||||
|
||||
#### Is this the only library available for navigation?
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"splash": {
|
||||
"image": "./assets/icons/splash.png"
|
||||
},
|
||||
"sdkVersion": "25.0.0",
|
||||
"sdkVersion": "26.0.0",
|
||||
"entryPoint": "./node_modules/react-native-scripts/build/bin/crna-entry.js",
|
||||
"packagerOpts": {
|
||||
"assetExts": [
|
||||
|
||||
@@ -25,12 +25,14 @@ import MultipleDrawer from './MultipleDrawer';
|
||||
import TabsInDrawer from './TabsInDrawer';
|
||||
import ModalStack from './ModalStack';
|
||||
import StacksInTabs from './StacksInTabs';
|
||||
import SwitchWithStacks from './SwitchWithStacks';
|
||||
import StacksOverTabs from './StacksOverTabs';
|
||||
import StacksWithKeys from './StacksWithKeys';
|
||||
import SimpleStack from './SimpleStack';
|
||||
import StackWithHeaderPreset from './StackWithHeaderPreset';
|
||||
import StackWithTranslucentHeader from './StackWithTranslucentHeader';
|
||||
import SimpleTabs from './SimpleTabs';
|
||||
import CustomTabNavigator from './CustomTabNavigator';
|
||||
import TabAnimations from './TabAnimations';
|
||||
import TabsWithNavigationFocus from './TabsWithNavigationFocus';
|
||||
|
||||
@@ -39,6 +41,10 @@ const ExampleInfo = {
|
||||
name: 'Stack Example',
|
||||
description: 'A card stack',
|
||||
},
|
||||
SwitchWithStacks: {
|
||||
name: 'Switch Example',
|
||||
description: 'A switch with stacks inside',
|
||||
},
|
||||
SimpleTabs: {
|
||||
name: 'Tabs Example',
|
||||
description: 'Tabs following platform conventions',
|
||||
@@ -67,9 +73,13 @@ const ExampleInfo = {
|
||||
name: 'Drawer + Tabs Example',
|
||||
description: 'A drawer combined with tabs',
|
||||
},
|
||||
CustomTabNavigator: {
|
||||
name: 'Custom Tab Navigator Example',
|
||||
description: 'Extending the tab navigator',
|
||||
},
|
||||
CustomTabs: {
|
||||
name: 'Custom Tabs',
|
||||
description: 'Custom tabs with tab router',
|
||||
name: 'Custom Tabs View',
|
||||
description: 'Custom tabs using TabRouter',
|
||||
},
|
||||
CustomTransitioner: {
|
||||
name: 'Custom Transitioner',
|
||||
@@ -116,21 +126,23 @@ const ExampleInfo = {
|
||||
};
|
||||
|
||||
const ExampleRoutes = {
|
||||
SimpleStack: SimpleStack,
|
||||
SimpleTabs: SimpleTabs,
|
||||
Drawer: Drawer,
|
||||
SimpleStack,
|
||||
SwitchWithStacks,
|
||||
SimpleTabs,
|
||||
Drawer,
|
||||
// MultipleDrawer: {
|
||||
// screen: MultipleDrawer,
|
||||
// },
|
||||
StackWithHeaderPreset: StackWithHeaderPreset,
|
||||
StackWithTranslucentHeader: StackWithTranslucentHeader,
|
||||
TabsInDrawer: TabsInDrawer,
|
||||
CustomTabs: CustomTabs,
|
||||
CustomTransitioner: CustomTransitioner,
|
||||
ModalStack: ModalStack,
|
||||
StacksWithKeys: StacksWithKeys,
|
||||
StacksInTabs: StacksInTabs,
|
||||
StacksOverTabs: StacksOverTabs,
|
||||
StackWithHeaderPreset,
|
||||
StackWithTranslucentHeader,
|
||||
TabsInDrawer,
|
||||
CustomTabs,
|
||||
CustomTransitioner,
|
||||
ModalStack,
|
||||
StacksWithKeys,
|
||||
StacksInTabs,
|
||||
StacksOverTabs,
|
||||
CustomTabNavigator,
|
||||
LinkStack: {
|
||||
screen: SimpleStack,
|
||||
path: 'people/Jordan',
|
||||
@@ -305,8 +317,7 @@ const AppNavigator = StackNavigator(
|
||||
}
|
||||
);
|
||||
|
||||
// export default () => <AppNavigator />;
|
||||
export default SimpleStack;
|
||||
export default () => <AppNavigator />;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
item: {
|
||||
|
||||
201
examples/NavigationPlayground/js/CustomTabNavigator.js
Normal file
201
examples/NavigationPlayground/js/CustomTabNavigator.js
Normal file
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {
|
||||
NavigationScreenProp,
|
||||
NavigationEventSubscription,
|
||||
} from 'react-navigation';
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
Platform,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, TabNavigator } from 'react-navigation';
|
||||
import Ionicons from 'react-native-vector-icons/Ionicons';
|
||||
import SampleText from './SampleText';
|
||||
|
||||
const MyNavScreen = ({ navigation, banner }) => (
|
||||
<SafeAreaView forceInset={{ horizontal: 'always', top: 'always' }}>
|
||||
<SampleText>{banner}</SampleText>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('Home')}
|
||||
title="Go to home tab"
|
||||
/>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('Settings')}
|
||||
title="Go to settings tab"
|
||||
/>
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
<StatusBar barStyle="default" />
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
||||
const MyHomeScreen = ({ navigation }) => (
|
||||
<MyNavScreen banner="Home Tab" navigation={navigation} />
|
||||
);
|
||||
|
||||
MyHomeScreen.navigationOptions = {
|
||||
tabBarTestIDProps: {
|
||||
testID: 'TEST_ID_HOME',
|
||||
accessibilityLabel: 'TEST_ID_HOME_ACLBL',
|
||||
},
|
||||
tabBarLabel: 'Home',
|
||||
tabBarIcon: ({ tintColor, focused }) => (
|
||||
<Ionicons
|
||||
name={focused ? 'ios-home' : 'ios-home-outline'}
|
||||
size={26}
|
||||
style={{ color: tintColor }}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
type MyPeopleScreenProps = {
|
||||
navigation: NavigationScreenProp<*>,
|
||||
};
|
||||
class MyPeopleScreen extends React.Component<MyPeopleScreenProps> {
|
||||
static navigationOptions = {
|
||||
tabBarLabel: 'People',
|
||||
tabBarIcon: ({ tintColor, focused }) => (
|
||||
<Ionicons
|
||||
name={focused ? 'ios-people' : 'ios-people-outline'}
|
||||
size={26}
|
||||
style={{ color: tintColor }}
|
||||
/>
|
||||
),
|
||||
};
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
return <MyNavScreen banner="People Tab" navigation={navigation} />;
|
||||
}
|
||||
}
|
||||
|
||||
type MyChatScreenProps = {
|
||||
navigation: NavigationScreenProp<*>,
|
||||
};
|
||||
class MyChatScreen extends React.Component<MyChatScreenProps> {
|
||||
static navigationOptions = {
|
||||
tabBarLabel: 'Chat',
|
||||
tabBarIcon: ({ tintColor, focused }) => (
|
||||
<Ionicons
|
||||
name={focused ? 'ios-chatboxes' : 'ios-chatboxes-outline'}
|
||||
size={26}
|
||||
style={{ color: tintColor }}
|
||||
/>
|
||||
),
|
||||
};
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
return <MyNavScreen banner="Chat Tab" navigation={navigation} />;
|
||||
}
|
||||
}
|
||||
|
||||
const MySettingsScreen = ({ navigation }) => (
|
||||
<MyNavScreen banner="Settings Tab" navigation={navigation} />
|
||||
);
|
||||
|
||||
MySettingsScreen.navigationOptions = {
|
||||
tabBarLabel: 'Settings',
|
||||
tabBarIcon: ({ tintColor, focused }) => (
|
||||
<Ionicons
|
||||
name={focused ? 'ios-settings' : 'ios-settings-outline'}
|
||||
size={26}
|
||||
style={{ color: tintColor }}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
const SimpleTabs = TabNavigator(
|
||||
{
|
||||
Home: {
|
||||
screen: MyHomeScreen,
|
||||
},
|
||||
People: {
|
||||
screen: MyPeopleScreen,
|
||||
},
|
||||
Chat: {
|
||||
screen: MyChatScreen,
|
||||
},
|
||||
Settings: {
|
||||
screen: MySettingsScreen,
|
||||
},
|
||||
},
|
||||
{
|
||||
navigationOptions: {
|
||||
tabBarVisible: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
type SimpleTabsContainerProps = {
|
||||
navigation: NavigationScreenProp<*>,
|
||||
};
|
||||
|
||||
const TabBarButton = ({ name, navigation }) => {
|
||||
const currentName = navigation.state.routes[navigation.state.index].routeName;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
navigation.navigate(name);
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{ color: currentName === name ? 'blue' : 'black', fontSize: 32 }}
|
||||
>
|
||||
{name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
class CustomTabBar extends React.Component<*> {
|
||||
render() {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
height: 40,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
}}
|
||||
>
|
||||
<TabBarButton navigation={this.props.navigation} name="Home" />
|
||||
<TabBarButton navigation={this.props.navigation} name="People" />
|
||||
<TabBarButton navigation={this.props.navigation} name="Chat" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SimpleTabsContainer extends React.Component<SimpleTabsContainerProps> {
|
||||
static router = {
|
||||
...SimpleTabs.router,
|
||||
getStateForAction(action, lastState) {
|
||||
// You can override the behavior navigation actions here, which are dispatched via navigation.dispatch, or via helpers like navigaiton.navigate.
|
||||
|
||||
// In this case we simply use the default behavior:
|
||||
const newState = SimpleTabs.router.getStateForAction(action, lastState);
|
||||
|
||||
console.log('Tab router action:', action, newState);
|
||||
return newState;
|
||||
},
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<SimpleTabs navigation={this.props.navigation} />
|
||||
<CustomTabBar navigation={this.props.navigation} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SimpleTabsContainer;
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
createNavigationContainer,
|
||||
SafeAreaView,
|
||||
TabRouter,
|
||||
addNavigationHelpers,
|
||||
} from 'react-navigation';
|
||||
import SampleText from './SampleText';
|
||||
|
||||
@@ -65,14 +66,19 @@ const CustomTabBar = ({ navigation }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const CustomTabView = ({ descriptors, navigation }) => {
|
||||
const CustomTabView = ({ router, navigation }) => {
|
||||
const { routes, index } = navigation.state;
|
||||
const descriptor = descriptors[routes[index].key];
|
||||
const ActiveScreen = descriptor.getComponent();
|
||||
const ActiveScreen = router.getComponentForRouteName(routes[index].routeName);
|
||||
return (
|
||||
<SafeAreaView forceInset={{ top: 'always' }}>
|
||||
<CustomTabBar navigation={navigation} />
|
||||
<ActiveScreen navigation={descriptor.navigation} />
|
||||
<ActiveScreen
|
||||
navigation={addNavigationHelpers({
|
||||
dispatch: navigation.dispatch,
|
||||
state: routes[index],
|
||||
})}
|
||||
screenProps={{}}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
@@ -99,7 +105,7 @@ const CustomTabRouter = TabRouter(
|
||||
);
|
||||
|
||||
const CustomTabs = createNavigationContainer(
|
||||
createNavigator(CustomTabView, CustomTabRouter, {})
|
||||
createNavigator(CustomTabRouter)(CustomTabView)
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
SafeAreaView,
|
||||
StackRouter,
|
||||
createNavigationContainer,
|
||||
addNavigationHelpers,
|
||||
createNavigator,
|
||||
} from 'react-navigation';
|
||||
import SampleText from './SampleText';
|
||||
@@ -44,12 +45,11 @@ const MySettingsScreen = ({ navigation }) => (
|
||||
|
||||
class CustomNavigationView extends Component {
|
||||
render() {
|
||||
const { navigation, router, descriptors } = this.props;
|
||||
const { navigation, router } = this.props;
|
||||
|
||||
return (
|
||||
<Transitioner
|
||||
configureTransition={this._configureTransition}
|
||||
descriptors={descriptors}
|
||||
navigation={navigation}
|
||||
render={this._render}
|
||||
/>
|
||||
@@ -86,10 +86,16 @@ class CustomNavigationView extends Component {
|
||||
transform: [{ scale: animatedValue }],
|
||||
};
|
||||
|
||||
const Scene = scene.descriptor.getComponent();
|
||||
// The prop `router` is populated when we call `createNavigator`.
|
||||
const Scene = router.getComponentForRouteName(scene.route.routeName);
|
||||
return (
|
||||
<Animated.View key={index} style={[styles.view, animation]}>
|
||||
<Scene navigation={scene.descriptor.navigation} />
|
||||
<Scene
|
||||
navigation={addNavigationHelpers({
|
||||
...navigation,
|
||||
state: routes[index],
|
||||
})}
|
||||
/>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
@@ -101,7 +107,7 @@ const CustomRouter = StackRouter({
|
||||
});
|
||||
|
||||
const CustomTransitioner = createNavigationContainer(
|
||||
createNavigator(CustomNavigationView, CustomRouter, {})
|
||||
createNavigator(CustomRouter)(CustomNavigationView)
|
||||
);
|
||||
|
||||
export default CustomTransitioner;
|
||||
|
||||
@@ -4,11 +4,7 @@
|
||||
|
||||
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';
|
||||
|
||||
@@ -16,7 +12,10 @@ const MyNavScreen = ({ navigation, banner }) => (
|
||||
<ScrollView>
|
||||
<SafeAreaView forceInset={{ top: 'always' }}>
|
||||
<SampleText>{banner}</SampleText>
|
||||
<Button onPress={() => navigation.openDrawer()} title="Open drawer" />
|
||||
<Button
|
||||
onPress={() => navigation.navigate('DrawerOpen')}
|
||||
title="Open drawer"
|
||||
/>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('Email')}
|
||||
title="Open other screen"
|
||||
@@ -77,6 +76,9 @@ const DrawerExample = DrawerNavigator(
|
||||
},
|
||||
},
|
||||
{
|
||||
drawerOpenRoute: 'DrawerOpen',
|
||||
drawerCloseRoute: 'DrawerClose',
|
||||
drawerToggleRoute: 'DrawerToggle',
|
||||
initialRouteName: 'Drafts',
|
||||
contentOptions: {
|
||||
activeTintColor: '#e91e63',
|
||||
|
||||
@@ -11,7 +11,10 @@ import SampleText from './SampleText';
|
||||
const MyNavScreen = ({ navigation, banner }) => (
|
||||
<ScrollView style={styles.container}>
|
||||
<SampleText>{banner}</SampleText>
|
||||
<Button onPress={() => navigation.openDrawer()} title="Open drawer" />
|
||||
<Button
|
||||
onPress={() => navigation.navigate('DrawerOpen')}
|
||||
title="Open drawer"
|
||||
/>
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
</ScrollView>
|
||||
);
|
||||
@@ -52,6 +55,9 @@ const DrawerExample = DrawerNavigator(
|
||||
},
|
||||
},
|
||||
{
|
||||
drawerOpenRoute: 'DrawerOpen',
|
||||
drawerCloseRoute: 'DrawerClose',
|
||||
drawerToggleRoute: 'DrawerToggle',
|
||||
initialRouteName: 'Drafts',
|
||||
contentOptions: {
|
||||
activeTintColor: '#e91e63',
|
||||
@@ -63,6 +69,10 @@ const MainDrawerExample = DrawerNavigator({
|
||||
Drafts: {
|
||||
screen: DrawerExample,
|
||||
},
|
||||
}, {
|
||||
drawerOpenRoute: 'DrawerOpen',
|
||||
drawerCloseRoute: 'DrawerClose',
|
||||
drawerToggleRoute: 'DrawerToggle',
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
||||
@@ -52,11 +52,6 @@ type MyPeopleScreenProps = {
|
||||
navigation: NavigationScreenProp<*>,
|
||||
};
|
||||
class MyPeopleScreen extends React.Component<MyPeopleScreenProps> {
|
||||
_s0: NavigationEventSubscription;
|
||||
_s1: NavigationEventSubscription;
|
||||
_s2: NavigationEventSubscription;
|
||||
_s3: NavigationEventSubscription;
|
||||
|
||||
static navigationOptions = {
|
||||
tabBarLabel: 'People',
|
||||
tabBarIcon: ({ tintColor, focused }) => (
|
||||
@@ -67,21 +62,6 @@ class MyPeopleScreen extends React.Component<MyPeopleScreenProps> {
|
||||
/>
|
||||
),
|
||||
};
|
||||
componentDidMount() {
|
||||
this._s0 = this.props.navigation.addListener('willFocus', this._onEvent);
|
||||
this._s1 = this.props.navigation.addListener('didFocus', this._onEvent);
|
||||
this._s2 = this.props.navigation.addListener('willBlur', this._onEvent);
|
||||
this._s3 = this.props.navigation.addListener('didBlur', this._onEvent);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this._s0.remove();
|
||||
this._s1.remove();
|
||||
this._s2.remove();
|
||||
this._s3.remove();
|
||||
}
|
||||
_onEvent = a => {
|
||||
console.log('EVENT ON PEOPLE TAB', a.type, a);
|
||||
};
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
return <MyNavScreen banner="People Tab" navigation={navigation} />;
|
||||
@@ -92,11 +72,6 @@ type MyChatScreenProps = {
|
||||
navigation: NavigationScreenProp<*>,
|
||||
};
|
||||
class MyChatScreen extends React.Component<MyChatScreenProps> {
|
||||
_s0: NavigationEventSubscription;
|
||||
_s1: NavigationEventSubscription;
|
||||
_s2: NavigationEventSubscription;
|
||||
_s3: NavigationEventSubscription;
|
||||
|
||||
static navigationOptions = {
|
||||
tabBarLabel: 'Chat',
|
||||
tabBarIcon: ({ tintColor, focused }) => (
|
||||
@@ -107,21 +82,6 @@ class MyChatScreen extends React.Component<MyChatScreenProps> {
|
||||
/>
|
||||
),
|
||||
};
|
||||
componentDidMount() {
|
||||
this._s0 = this.props.navigation.addListener('willFocus', this._onEvent);
|
||||
this._s1 = this.props.navigation.addListener('didFocus', this._onEvent);
|
||||
this._s2 = this.props.navigation.addListener('willBlur', this._onEvent);
|
||||
this._s3 = this.props.navigation.addListener('didBlur', this._onEvent);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this._s0.remove();
|
||||
this._s1.remove();
|
||||
this._s2.remove();
|
||||
this._s3.remove();
|
||||
}
|
||||
_onEvent = a => {
|
||||
console.log('EVENT ON CHAT TAB', a.type, a);
|
||||
};
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
return <MyNavScreen banner="Chat Tab" navigation={navigation} />;
|
||||
@@ -171,35 +131,4 @@ const SimpleTabs = TabNavigator(
|
||||
}
|
||||
);
|
||||
|
||||
type SimpleTabsContainerProps = {
|
||||
navigation: NavigationScreenProp<*>,
|
||||
};
|
||||
|
||||
class SimpleTabsContainer extends React.Component<SimpleTabsContainerProps> {
|
||||
static router = SimpleTabs.router;
|
||||
_s0: NavigationEventSubscription;
|
||||
_s1: NavigationEventSubscription;
|
||||
_s2: NavigationEventSubscription;
|
||||
_s3: NavigationEventSubscription;
|
||||
|
||||
componentDidMount() {
|
||||
this._s0 = this.props.navigation.addListener('willFocus', this._onAction);
|
||||
this._s1 = this.props.navigation.addListener('didFocus', this._onAction);
|
||||
this._s2 = this.props.navigation.addListener('willBlur', this._onAction);
|
||||
this._s3 = this.props.navigation.addListener('didBlur', this._onAction);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this._s0.remove();
|
||||
this._s1.remove();
|
||||
this._s2.remove();
|
||||
this._s3.remove();
|
||||
}
|
||||
_onAction = a => {
|
||||
console.log('TABS EVENT', a.type, a);
|
||||
};
|
||||
render() {
|
||||
return <SimpleTabs navigation={this.props.navigation} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default SimpleTabsContainer;
|
||||
export default SimpleTabs;
|
||||
|
||||
@@ -119,6 +119,9 @@ const StacksInTabs = TabNavigator(
|
||||
tabBarPosition: 'bottom',
|
||||
animationEnabled: false,
|
||||
swipeEnabled: false,
|
||||
tabBarOptions: {
|
||||
showLabel: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
121
examples/NavigationPlayground/js/SwitchWithStacks.js
Normal file
121
examples/NavigationPlayground/js/SwitchWithStacks.js
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
AsyncStorage,
|
||||
Button,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { StackNavigator, SwitchNavigator } from 'react-navigation';
|
||||
|
||||
class SignInScreen extends React.Component<any, any> {
|
||||
static navigationOptions = {
|
||||
title: 'Please sign in',
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Button title="Sign in!" onPress={this._signInAsync} />
|
||||
<Button
|
||||
title="Go back to other examples"
|
||||
onPress={() => this.props.navigation.goBack(null)}
|
||||
/>
|
||||
<StatusBar barStyle="default" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
_signInAsync = async () => {
|
||||
await AsyncStorage.setItem('userToken', 'abc');
|
||||
this.props.navigation.navigate('App');
|
||||
};
|
||||
}
|
||||
|
||||
class HomeScreen extends React.Component<any, any> {
|
||||
static navigationOptions = {
|
||||
title: 'Welcome to the app!',
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Button title="Show me more of the app" onPress={this._showMoreApp} />
|
||||
<Button title="Actually, sign me out :)" onPress={this._signOutAsync} />
|
||||
<StatusBar barStyle="default" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
_showMoreApp = () => {
|
||||
this.props.navigation.navigate('Other');
|
||||
};
|
||||
|
||||
_signOutAsync = async () => {
|
||||
await AsyncStorage.clear();
|
||||
this.props.navigation.navigate('Auth');
|
||||
};
|
||||
}
|
||||
|
||||
class OtherScreen extends React.Component<any, any> {
|
||||
static navigationOptions = {
|
||||
title: 'Lots of features here',
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Button title="I'm done, sign me out" onPress={this._signOutAsync} />
|
||||
<StatusBar barStyle="default" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
_signOutAsync = async () => {
|
||||
await AsyncStorage.clear();
|
||||
this.props.navigation.navigate('Auth');
|
||||
};
|
||||
}
|
||||
|
||||
class LoadingScreen extends React.Component<any, any> {
|
||||
componentDidMount() {
|
||||
this._bootstrapAsync();
|
||||
}
|
||||
|
||||
_bootstrapAsync = async () => {
|
||||
const userToken = await AsyncStorage.getItem('userToken');
|
||||
let initialRouteName = userToken ? 'App' : 'Auth';
|
||||
this.props.navigation.navigate(initialRouteName);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ActivityIndicator />
|
||||
<StatusBar barStyle="default" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
const AppStack = StackNavigator({ Home: HomeScreen, Other: OtherScreen });
|
||||
const AuthStack = StackNavigator({ SignIn: SignInScreen });
|
||||
|
||||
export default SwitchNavigator({
|
||||
Loading: LoadingScreen,
|
||||
App: AppStack,
|
||||
Auth: AuthStack,
|
||||
});
|
||||
@@ -4,7 +4,12 @@
|
||||
|
||||
import React from 'react';
|
||||
import { Button, SafeAreaView, Text } from 'react-native';
|
||||
import { TabNavigator, withNavigationFocus } from 'react-navigation';
|
||||
import {
|
||||
TabNavigator,
|
||||
TabBarTop,
|
||||
StackNavigator,
|
||||
withNavigationFocus,
|
||||
} from 'react-navigation';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
|
||||
import SampleText from './SampleText';
|
||||
@@ -92,10 +97,35 @@ const TabsWithNavigationFocus = TabNavigator(
|
||||
},
|
||||
},
|
||||
{
|
||||
tabBarPosition: 'bottom',
|
||||
tabBarComponent: TabBarTop,
|
||||
tabBarPosition: 'top',
|
||||
animationEnabled: true,
|
||||
swipeEnabled: true,
|
||||
}
|
||||
);
|
||||
|
||||
export default TabsWithNavigationFocus;
|
||||
const Stack = StackNavigator(
|
||||
{
|
||||
TabsWithNavigationFocus,
|
||||
},
|
||||
{
|
||||
navigationOptions: {
|
||||
headerTitle: 'Navigation focus example',
|
||||
headerLeft: null,
|
||||
headerTitleStyle: {
|
||||
flex: 1,
|
||||
textAlign: 'left',
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
headerTintColor: '#fff',
|
||||
headerStyle: {
|
||||
backgroundColor: '#2196f3',
|
||||
borderBottomWidth: 0,
|
||||
elevation: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default Stack;
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
"test": "node node_modules/jest/bin/jest.js && flow"
|
||||
},
|
||||
"dependencies": {
|
||||
"expo": "^25.0.0",
|
||||
"react": "16.2.0",
|
||||
"react-native": "^0.52.0",
|
||||
"expo": "^26.0.0",
|
||||
"react": "16.3.0-alpha.1",
|
||||
"react-native": "^0.54.0",
|
||||
"react-native-iphone-x-helper": "^1.0.2",
|
||||
"react-navigation": "link:../.."
|
||||
},
|
||||
@@ -22,9 +22,9 @@
|
||||
"babel-plugin-transform-remove-console": "^6.9.0",
|
||||
"flow-bin": "^0.61.0",
|
||||
"jest": "^21.0.1",
|
||||
"jest-expo": "^25.1.0",
|
||||
"jest-expo": "^26.0.0",
|
||||
"react-native-scripts": "^1.5.0",
|
||||
"react-test-renderer": "16.0.0"
|
||||
"react-test-renderer": "16.3.0-alpha.1"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "jest-expo",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@
|
||||
"react": "16.2.0",
|
||||
"react-native": "^0.52.0",
|
||||
"react-navigation": "link:../..",
|
||||
"react-navigation-redux-helpers": "^1.0.0",
|
||||
"react-navigation-redux-helpers": "^1.0.3",
|
||||
"react-redux": "^5.0.6",
|
||||
"redux": "^3.7.2"
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { addNavigationHelpers, StackNavigator } from 'react-navigation';
|
||||
import { initializeListeners } from 'react-navigation-redux-helpers';
|
||||
|
||||
import LoginScreen from '../components/LoginScreen';
|
||||
import MainScreen from '../components/MainScreen';
|
||||
@@ -20,6 +21,10 @@ class AppWithNavigationState extends React.Component {
|
||||
nav: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
initializeListeners('root', this.props.nav);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dispatch, nav } = this.props;
|
||||
return (
|
||||
|
||||
98
flow/react-navigation.js
vendored
98
flow/react-navigation.js
vendored
@@ -269,8 +269,7 @@ declare module 'react-navigation' {
|
||||
|
||||
declare export type NavigationComponent =
|
||||
| NavigationScreenComponent<NavigationRoute, *, *>
|
||||
| NavigationContainer<*, *, *>
|
||||
| any;
|
||||
| NavigationContainer<NavigationStateRoute, *, *>;
|
||||
|
||||
declare export type NavigationScreenComponent<
|
||||
Route: NavigationRoute,
|
||||
@@ -296,7 +295,6 @@ declare module 'react-navigation' {
|
||||
} & NavigationScreenRouteConfig);
|
||||
|
||||
declare export type NavigationScreenRouteConfig =
|
||||
| NavigationComponent
|
||||
| {
|
||||
screen: NavigationComponent,
|
||||
}
|
||||
@@ -380,6 +378,20 @@ declare module 'react-navigation' {
|
||||
...NavigationStackRouterConfig,
|
||||
|};
|
||||
|
||||
/**
|
||||
* Switch Navigator
|
||||
*/
|
||||
|
||||
declare export type NavigationSwitchRouterConfig = {|
|
||||
initialRouteName?: string,
|
||||
initialRouteParams?: NavigationParams,
|
||||
paths?: NavigationPathsConfig,
|
||||
navigationOptions?: NavigationScreenConfig<*>,
|
||||
order?: Array<string>,
|
||||
backBehavior?: 'none' | 'initialRoute', // defaults to `'none'`
|
||||
resetOnBlur?: boolean, // defaults to `true`
|
||||
|};
|
||||
|
||||
/**
|
||||
* Tab Navigator
|
||||
*/
|
||||
@@ -391,7 +403,6 @@ declare module 'react-navigation' {
|
||||
navigationOptions?: NavigationScreenConfig<*>,
|
||||
// todo: type these as the real route names rather than 'string'
|
||||
order?: Array<string>,
|
||||
|
||||
// Does the back button cause the router to switch to the initial tab
|
||||
backBehavior?: 'none' | 'initialRoute', // defaults `initialRoute`
|
||||
|};
|
||||
@@ -504,6 +515,29 @@ 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
|
||||
*/
|
||||
@@ -519,7 +553,7 @@ declare module 'react-navigation' {
|
||||
|
||||
declare export type NavigationContainerProps<S: {}, O: {}> = $Shape<{
|
||||
uriPrefix?: string | RegExp,
|
||||
onNavigationStateChange?: (
|
||||
onNavigationStateChange?: ?(
|
||||
NavigationState,
|
||||
NavigationState,
|
||||
NavigationAction
|
||||
@@ -678,6 +712,10 @@ 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',
|
||||
@@ -725,24 +763,23 @@ declare module 'react-navigation' {
|
||||
declare type _RouterProp<S: NavigationState, O: {}> = {
|
||||
router: NavigationRouter<S, O>,
|
||||
};
|
||||
|
||||
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>,
|
||||
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>
|
||||
>(
|
||||
router: NavigationRouter<S, O>,
|
||||
routeConfigs?: NavigationRouteConfigMap,
|
||||
navigatorConfig?: NavigatorConfig
|
||||
): any;
|
||||
): _NavigatorCreator<NavigationViewProps, S, O>;
|
||||
|
||||
declare export function StackNavigator(
|
||||
routeConfigMap: NavigationRouteConfigMap,
|
||||
@@ -772,11 +809,21 @@ declare module 'react-navigation' {
|
||||
routeConfigs: NavigationRouteConfigMap,
|
||||
config?: _TabNavigatorConfig
|
||||
): NavigationContainer<*, *, *>;
|
||||
declare type _SwitchNavigatorConfig = {|
|
||||
...NavigationSwitchRouterConfig,
|
||||
|};
|
||||
declare export function SwitchNavigator(
|
||||
routeConfigs: NavigationRouteConfigMap,
|
||||
config?: _SwitchNavigatorConfig
|
||||
): NavigationContainer<*, *, *>;
|
||||
|
||||
declare type _DrawerViewConfig = {|
|
||||
drawerLockMode?: 'unlocked' | 'locked-closed' | 'locked-open',
|
||||
drawerWidth?: number | (() => number),
|
||||
drawerPosition?: 'left' | 'right',
|
||||
drawerOpenRoute?: string,
|
||||
drawerCloseRoute?: string,
|
||||
drawerToggleRoute?: string,
|
||||
contentComponent?: React$ElementType,
|
||||
contentOptions?: {},
|
||||
style?: ViewStyleProp,
|
||||
@@ -878,12 +925,14 @@ declare module 'react-navigation' {
|
||||
vertical?: _SafeAreaViewForceInsetValue,
|
||||
horizontal?: _SafeAreaViewForceInsetValue,
|
||||
},
|
||||
children: React$Node,
|
||||
children?: React$Node,
|
||||
style?: AnimatedViewStyleProp,
|
||||
};
|
||||
declare export var SafeAreaView: React$ComponentType<_SafeAreaViewProps>;
|
||||
|
||||
declare export var Header: React$ComponentType<HeaderProps>;
|
||||
declare export var Header: React$ComponentType<HeaderProps> & {
|
||||
HEIGHT: number,
|
||||
};
|
||||
|
||||
declare type _HeaderTitleProps = {
|
||||
children: React$Node,
|
||||
@@ -909,6 +958,9 @@ 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,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-navigation",
|
||||
"version": "1.2.1",
|
||||
"version": "1.5.10",
|
||||
"description": "Routing and navigation for your React Native apps",
|
||||
"main": "src/react-navigation.js",
|
||||
"repository": {
|
||||
@@ -32,9 +32,10 @@
|
||||
"hoist-non-react-statics": "^2.2.0",
|
||||
"path-to-regexp": "^1.7.0",
|
||||
"prop-types": "^15.5.10",
|
||||
"react-lifecycles-compat": "^1.0.2",
|
||||
"react-native-drawer-layout-polyfill": "^1.3.2",
|
||||
"react-native-safe-area-view": "^0.7.0",
|
||||
"react-native-tab-view": "^0.0.74"
|
||||
"react-native-tab-view": "github:react-navigation/react-native-tab-view"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.24.1",
|
||||
|
||||
@@ -9,9 +9,6 @@ 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;
|
||||
@@ -110,16 +107,6 @@ 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,
|
||||
@@ -133,9 +120,6 @@ export default {
|
||||
SET_PARAMS,
|
||||
URI,
|
||||
COMPLETE_TRANSITION,
|
||||
OPEN_DRAWER,
|
||||
CLOSE_DRAWER,
|
||||
TOGGLE_DRAWER,
|
||||
|
||||
// Action creators
|
||||
back,
|
||||
@@ -149,7 +133,4 @@ 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/createStackNavigator';
|
||||
import StackNavigator from '../navigators/StackNavigator';
|
||||
|
||||
const FooScreen = () => <div />;
|
||||
const BarScreen = () => <div />;
|
||||
|
||||
@@ -8,6 +8,7 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(NavigationStateUtils.get(state, 'a')).toEqual({
|
||||
key: 'a',
|
||||
@@ -20,6 +21,7 @@ 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);
|
||||
@@ -30,6 +32,7 @@ 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);
|
||||
@@ -40,9 +43,11 @@ 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(
|
||||
@@ -54,6 +59,7 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(() =>
|
||||
NavigationStateUtils.push(state, { key: 'a', routeName })
|
||||
@@ -65,10 +71,12 @@ 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);
|
||||
});
|
||||
@@ -77,6 +85,7 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(NavigationStateUtils.pop(state)).toBe(state);
|
||||
});
|
||||
@@ -86,10 +95,12 @@ 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);
|
||||
@@ -99,6 +110,7 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(() => NavigationStateUtils.jumpToIndex(state, 2)).toThrow();
|
||||
});
|
||||
@@ -107,10 +119,12 @@ 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);
|
||||
@@ -120,6 +134,7 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(() => NavigationStateUtils.jumpTo(state, 'c')).toThrow();
|
||||
});
|
||||
@@ -128,10 +143,12 @@ 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);
|
||||
@@ -141,10 +158,12 @@ 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);
|
||||
@@ -155,10 +174,12 @@ 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 })
|
||||
@@ -169,10 +190,12 @@ 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 })
|
||||
@@ -183,6 +206,7 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(
|
||||
NavigationStateUtils.replaceAtIndex(state, 1, state.routes[1])
|
||||
@@ -194,10 +218,12 @@ 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, [
|
||||
@@ -215,10 +241,12 @@ 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,8 +11,10 @@ test('child action events only flow when focused', () => {
|
||||
};
|
||||
const subscriptionRemove = () => {};
|
||||
parentSubscriber.mockReturnValueOnce({ remove: subscriptionRemove });
|
||||
const childEventSubscriber = getChildEventSubscriber(parentSubscriber, 'key1')
|
||||
.addListener;
|
||||
const childEventSubscriber = getChildEventSubscriber(
|
||||
parentSubscriber,
|
||||
'key1'
|
||||
);
|
||||
const testState = {
|
||||
key: 'foo',
|
||||
routeName: 'FooRoute',
|
||||
@@ -64,9 +66,11 @@ test('grandchildren subscription', () => {
|
||||
const parentSubscriber = getChildEventSubscriber(
|
||||
grandParentSubscriber,
|
||||
'parent'
|
||||
).addListener;
|
||||
const childEventSubscriber = getChildEventSubscriber(parentSubscriber, 'key1')
|
||||
.addListener;
|
||||
);
|
||||
const childEventSubscriber = getChildEventSubscriber(
|
||||
parentSubscriber,
|
||||
'key1'
|
||||
);
|
||||
const parentBlurState = {
|
||||
key: 'foo',
|
||||
routeName: 'FooRoute',
|
||||
@@ -131,9 +135,11 @@ test('grandchildren transitions', () => {
|
||||
const parentSubscriber = getChildEventSubscriber(
|
||||
grandParentSubscriber,
|
||||
'parent'
|
||||
).addListener;
|
||||
const childEventSubscriber = getChildEventSubscriber(parentSubscriber, 'key1')
|
||||
.addListener;
|
||||
);
|
||||
const childEventSubscriber = getChildEventSubscriber(
|
||||
parentSubscriber,
|
||||
'key1'
|
||||
);
|
||||
const makeFakeState = (childIndex, childIsTransitioning) => ({
|
||||
index: 1,
|
||||
isTransitioning: false,
|
||||
@@ -224,9 +230,11 @@ test('grandchildren pass through transitions', () => {
|
||||
const parentSubscriber = getChildEventSubscriber(
|
||||
grandParentSubscriber,
|
||||
'parent'
|
||||
).addListener;
|
||||
const childEventSubscriber = getChildEventSubscriber(parentSubscriber, 'key1')
|
||||
.addListener;
|
||||
);
|
||||
const childEventSubscriber = getChildEventSubscriber(
|
||||
parentSubscriber,
|
||||
'key1'
|
||||
);
|
||||
const makeFakeState = (childIndex, childIsTransitioning) => ({
|
||||
index: childIndex,
|
||||
isTransitioning: childIsTransitioning,
|
||||
@@ -314,8 +322,10 @@ test('child focus with transition', () => {
|
||||
};
|
||||
const subscriptionRemove = () => {};
|
||||
parentSubscriber.mockReturnValueOnce({ remove: subscriptionRemove });
|
||||
const childEventSubscriber = getChildEventSubscriber(parentSubscriber, 'key1')
|
||||
.addListener;
|
||||
const childEventSubscriber = getChildEventSubscriber(
|
||||
parentSubscriber,
|
||||
'key1'
|
||||
);
|
||||
const randomAction = { type: 'FooAction' };
|
||||
const testState = {
|
||||
key: 'foo',
|
||||
@@ -407,8 +417,10 @@ test('child focus with immediate transition', () => {
|
||||
};
|
||||
const subscriptionRemove = () => {};
|
||||
parentSubscriber.mockReturnValueOnce({ remove: subscriptionRemove });
|
||||
const childEventSubscriber = getChildEventSubscriber(parentSubscriber, 'key1')
|
||||
.addListener;
|
||||
const childEventSubscriber = getChildEventSubscriber(
|
||||
parentSubscriber,
|
||||
'key1'
|
||||
);
|
||||
const randomAction = { type: 'FooAction' };
|
||||
const testState = {
|
||||
key: 'foo',
|
||||
|
||||
@@ -85,9 +85,5 @@ export default function(navigation) {
|
||||
key: navigation.state.key,
|
||||
})
|
||||
),
|
||||
|
||||
openDrawer: () => navigation.dispatch(NavigationActions.openDrawer()),
|
||||
closeDrawer: () => navigation.dispatch(NavigationActions.closeDrawer()),
|
||||
toggleDrawer: () => navigation.dispatch(NavigationActions.toggleDrawer()),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* Based on the 'action' events that get fired for this navigation state, this utility will fire
|
||||
* focus and blur events for this child
|
||||
*/
|
||||
|
||||
export default function getChildEventSubscriber(addListener, key) {
|
||||
const actionSubscribers = new Set();
|
||||
const willFocusSubscribers = new Set();
|
||||
@@ -11,18 +12,6 @@ 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':
|
||||
@@ -55,6 +44,10 @@ export default function getChildEventSubscriber(addListener, key) {
|
||||
// considered blurred
|
||||
let lastEmittedEvent = 'didBlur';
|
||||
|
||||
const cleanup = () => {
|
||||
upstreamSubscribers.forEach(subs => subs && subs.remove());
|
||||
};
|
||||
|
||||
const upstreamEvents = [
|
||||
'willFocus',
|
||||
'didFocus',
|
||||
@@ -84,7 +77,7 @@ export default function getChildEventSubscriber(addListener, key) {
|
||||
action,
|
||||
type: eventName,
|
||||
};
|
||||
const isTransitioning = !!state && !!state.transitioningFromKey;
|
||||
const isTransitioning = !!state && state.isTransitioning;
|
||||
|
||||
const previouslyLastEmittedEvent = lastEmittedEvent;
|
||||
|
||||
@@ -141,18 +134,15 @@ export default function getChildEventSubscriber(addListener, key) {
|
||||
})
|
||||
);
|
||||
|
||||
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 };
|
||||
},
|
||||
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 };
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import SafeAreaView from 'react-native-safe-area-view';
|
||||
|
||||
import createNavigator from './createNavigator';
|
||||
import createNavigationContainer from '../createNavigationContainer';
|
||||
import DrawerRouter from '../routers/DrawerRouter';
|
||||
import TabRouter from '../routers/TabRouter';
|
||||
import DrawerScreen from '../views/Drawer/DrawerScreen';
|
||||
import DrawerView from '../views/Drawer/DrawerView';
|
||||
import DrawerItems from '../views/Drawer/DrawerNavigatorItems';
|
||||
@@ -38,6 +38,9 @@ const DefaultDrawerConfig = {
|
||||
return Math.min(smallerAxisSize - appBarHeight, maxWidth);
|
||||
},
|
||||
contentComponent: defaultContentComponent,
|
||||
drawerOpenRoute: 'DrawerOpen',
|
||||
drawerCloseRoute: 'DrawerClose',
|
||||
drawerToggleRoute: 'DrawerToggle',
|
||||
drawerPosition: 'left',
|
||||
drawerBackgroundColor: 'white',
|
||||
useNativeAnimations: true,
|
||||
@@ -45,25 +48,58 @@ const DefaultDrawerConfig = {
|
||||
|
||||
const DrawerNavigator = (routeConfigs, config = {}) => {
|
||||
const mergedConfig = { ...DefaultDrawerConfig, ...config };
|
||||
|
||||
const {
|
||||
order,
|
||||
paths,
|
||||
initialRouteName,
|
||||
backBehavior,
|
||||
...drawerConfig
|
||||
containerConfig,
|
||||
drawerWidth,
|
||||
drawerLockMode,
|
||||
contentComponent,
|
||||
contentOptions,
|
||||
drawerPosition,
|
||||
useNativeAnimations,
|
||||
drawerBackgroundColor,
|
||||
drawerOpenRoute,
|
||||
drawerCloseRoute,
|
||||
drawerToggleRoute,
|
||||
...tabsConfig
|
||||
} = mergedConfig;
|
||||
|
||||
const routerConfig = {
|
||||
order,
|
||||
paths,
|
||||
initialRouteName,
|
||||
backBehavior,
|
||||
};
|
||||
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 drawerRouter = DrawerRouter(routeConfigs, routerConfig);
|
||||
|
||||
const navigator = createNavigator(DrawerView, drawerRouter, drawerConfig);
|
||||
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}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
return createNavigationContainer(navigator);
|
||||
};
|
||||
|
||||
59
src/navigators/StackNavigator.js
Normal file
59
src/navigators/StackNavigator.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import createNavigationContainer from '../createNavigationContainer';
|
||||
import createNavigator from './createNavigator';
|
||||
import CardStackTransitioner from '../views/CardStack/CardStackTransitioner';
|
||||
import StackRouter from '../routers/StackRouter';
|
||||
import NavigationActions from '../NavigationActions';
|
||||
|
||||
// A stack navigators props are the intersection between
|
||||
// the base navigator props (navgiation, screenProps, etc)
|
||||
// and the view's props
|
||||
|
||||
export default (routeConfigMap, stackConfig = {}) => {
|
||||
const {
|
||||
initialRouteKey,
|
||||
initialRouteName,
|
||||
initialRouteParams,
|
||||
paths,
|
||||
headerMode,
|
||||
headerTransitionPreset,
|
||||
mode,
|
||||
cardStyle,
|
||||
transitionConfig,
|
||||
onTransitionStart,
|
||||
onTransitionEnd,
|
||||
navigationOptions,
|
||||
} = stackConfig;
|
||||
|
||||
const stackRouterConfig = {
|
||||
initialRouteKey,
|
||||
initialRouteName,
|
||||
initialRouteParams,
|
||||
paths,
|
||||
navigationOptions,
|
||||
};
|
||||
|
||||
const router = StackRouter(routeConfigMap, stackRouterConfig);
|
||||
|
||||
// Create a navigator with CardStackTransitioner as the view
|
||||
const navigator = createNavigator(router, routeConfigMap, stackConfig)(
|
||||
props => (
|
||||
<CardStackTransitioner
|
||||
{...props}
|
||||
headerMode={headerMode}
|
||||
headerTransitionPreset={headerTransitionPreset}
|
||||
mode={mode}
|
||||
cardStyle={cardStyle}
|
||||
transitionConfig={transitionConfig}
|
||||
onTransitionStart={onTransitionStart}
|
||||
onTransitionEnd={(lastTransition, transition) => {
|
||||
const { state, dispatch } = props.navigation;
|
||||
dispatch(NavigationActions.completeTransition({ key: state.key }));
|
||||
onTransitionEnd && onTransitionEnd(lastTransition, transition);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
return createNavigationContainer(navigator);
|
||||
};
|
||||
15
src/navigators/SwitchNavigator.js
Normal file
15
src/navigators/SwitchNavigator.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import SwitchRouter from '../routers/SwitchRouter';
|
||||
import SwitchView from '../views/SwitchView/SwitchView';
|
||||
import createNavigationContainer from '../createNavigationContainer';
|
||||
import createNavigator from '../navigators/createNavigator';
|
||||
|
||||
export default (routeConfigMap, switchConfig = {}) => {
|
||||
const router = SwitchRouter(routeConfigMap, switchConfig);
|
||||
|
||||
const navigator = createNavigator(router, routeConfigMap, switchConfig)(
|
||||
props => <SwitchView {...props} />
|
||||
);
|
||||
|
||||
return createNavigationContainer(navigator);
|
||||
};
|
||||
@@ -8,13 +8,42 @@ 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 tabsConfig = { ...TabNavigator.Presets.Default, ...config };
|
||||
const mergedConfig = { ...TabNavigator.Presets.Default, ...config };
|
||||
const {
|
||||
tabBarComponent,
|
||||
tabBarPosition,
|
||||
tabBarOptions,
|
||||
lazy,
|
||||
removeClippedSubviews,
|
||||
swipeEnabled,
|
||||
animationEnabled,
|
||||
configureTransition,
|
||||
initialLayout,
|
||||
...tabsConfig
|
||||
} = mergedConfig;
|
||||
|
||||
const router = TabRouter(routeConfigs, tabsConfig);
|
||||
|
||||
const navigator = createNavigator(TabView, router, 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}
|
||||
/>
|
||||
));
|
||||
|
||||
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 '../createStackNavigator';
|
||||
import StackNavigator from '../StackNavigator';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
header: {
|
||||
|
||||
18
src/navigators/__tests__/SwitchNavigator-test.js
Normal file
18
src/navigators/__tests__/SwitchNavigator-test.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import React, { Component } from 'react';
|
||||
import { View } from 'react-native';
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import SwitchNavigator from '../SwitchNavigator';
|
||||
|
||||
const A = () => <View />;
|
||||
const B = () => <View />;
|
||||
const routeConfig = { A, B };
|
||||
|
||||
describe('SwitchNavigator', () => {
|
||||
it('renders successfully', () => {
|
||||
const MySwitchNavigator = SwitchNavigator(routeConfig);
|
||||
const rendered = renderer.create(<MySwitchNavigator />).toJSON();
|
||||
|
||||
expect(rendered).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import React, { Component } from 'react';
|
||||
import { View } from 'react-native';
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import TabNavigator from '../createTabNavigator';
|
||||
import TabNavigator from '../TabNavigator';
|
||||
|
||||
class HomeScreen extends Component {
|
||||
static navigationOptions = ({ navigation }) => ({
|
||||
|
||||
@@ -48,7 +48,7 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
|
||||
pointerEvents="auto"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#E9E9EF",
|
||||
"backgroundColor": "#EFEFF4",
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"opacity": 1,
|
||||
@@ -234,7 +234,7 @@ exports[`StackNavigator renders successfully 1`] = `
|
||||
pointerEvents="auto"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#E9E9EF",
|
||||
"backgroundColor": "#EFEFF4",
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"opacity": 1,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SwitchNavigator renders successfully 1`] = `<View />`;
|
||||
@@ -137,9 +137,15 @@ exports[`TabNavigator renders successfully 1`] = `
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flexGrow": 1,
|
||||
}
|
||||
Array [
|
||||
Object {
|
||||
"height": 29,
|
||||
},
|
||||
false,
|
||||
Object {
|
||||
"flexGrow": 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
|
||||
@@ -1,81 +1,19 @@
|
||||
import React from 'react';
|
||||
|
||||
import getChildEventSubscriber from '../getChildEventSubscriber';
|
||||
import addNavigationHelpers from '../addNavigationHelpers';
|
||||
/**
|
||||
* 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;
|
||||
|
||||
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];
|
||||
}
|
||||
});
|
||||
render() {
|
||||
return <NavigationView {...this.props} router={router} />;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
return Navigator;
|
||||
};
|
||||
}
|
||||
|
||||
export default createNavigator;
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
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;
|
||||
26
src/react-navigation.js
vendored
26
src/react-navigation.js
vendored
@@ -20,10 +20,13 @@ module.exports = {
|
||||
return require('./navigators/createNavigator').default;
|
||||
},
|
||||
get StackNavigator() {
|
||||
return require('./navigators/createStackNavigator').default;
|
||||
return require('./navigators/StackNavigator').default;
|
||||
},
|
||||
get SwitchNavigator() {
|
||||
return require('./navigators/SwitchNavigator').default;
|
||||
},
|
||||
get TabNavigator() {
|
||||
return require('./navigators/createTabNavigator').default;
|
||||
return require('./navigators/TabNavigator').default;
|
||||
},
|
||||
get DrawerNavigator() {
|
||||
return require('./navigators/DrawerNavigator').default;
|
||||
@@ -36,16 +39,22 @@ module.exports = {
|
||||
get TabRouter() {
|
||||
return require('./routers/TabRouter').default;
|
||||
},
|
||||
get SwitchRouter() {
|
||||
return require('./routers/SwitchRouter').default;
|
||||
},
|
||||
|
||||
// Views
|
||||
get Transitioner() {
|
||||
return require('./views/Transitioner').default;
|
||||
},
|
||||
get StackView() {
|
||||
return require('./views/StackView/StackView').default;
|
||||
get CardStackTransitioner() {
|
||||
return require('./views/CardStack/CardStackTransitioner').default;
|
||||
},
|
||||
get StackViewCard() {
|
||||
return require('./views/StackView/StackViewCard').default;
|
||||
get CardStack() {
|
||||
return require('./views/CardStack/CardStack').default;
|
||||
},
|
||||
get Card() {
|
||||
return require('./views/CardStack/Card').default;
|
||||
},
|
||||
get SafeAreaView() {
|
||||
return require('react-native-safe-area-view').default;
|
||||
@@ -81,6 +90,11 @@ module.exports = {
|
||||
return require('./views/TabView/TabBarBottom').default;
|
||||
},
|
||||
|
||||
// SwitchView
|
||||
get SwitchView() {
|
||||
return require('./views/SwitchView/SwitchView').default;
|
||||
},
|
||||
|
||||
// HOCs
|
||||
get withNavigation() {
|
||||
return require('./views/withNavigation').default;
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
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,6 +5,7 @@ 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';
|
||||
|
||||
@@ -65,7 +66,7 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
}
|
||||
return {
|
||||
key: 'StackRouterRoot',
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
index: 0,
|
||||
routes: [
|
||||
{
|
||||
@@ -100,7 +101,7 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
};
|
||||
return {
|
||||
key: 'StackRouterRoot',
|
||||
transitioningFromKey: false,
|
||||
isTransitioning: false,
|
||||
index: 0,
|
||||
routes: [route],
|
||||
};
|
||||
@@ -157,7 +158,6 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
if (!state) {
|
||||
return getInitialState(action);
|
||||
}
|
||||
const lastRouteKey = state.routes[state.index].key;
|
||||
|
||||
// Check if the focused child scene wants to handle the action, as long as
|
||||
// it is not a reset to the root stack
|
||||
@@ -222,13 +222,13 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
},
|
||||
};
|
||||
}
|
||||
// Return state with new index. Change transitioningFromKey only if index has changed
|
||||
// Return state with new index. Change isTransitioning only if index has changed
|
||||
return {
|
||||
...state,
|
||||
transitioningFromKey:
|
||||
isTransitioning:
|
||||
state.index !== lastRouteIndex
|
||||
? action.immediate !== true ? lastRouteKey : null
|
||||
: null,
|
||||
? action.immediate !== true
|
||||
: undefined,
|
||||
index: lastRouteIndex,
|
||||
routes,
|
||||
};
|
||||
@@ -254,7 +254,7 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
}
|
||||
return {
|
||||
...StateUtils.push(state, route),
|
||||
transitioningFromKey: action.immediate !== true ? lastRouteKey : null,
|
||||
isTransitioning: action.immediate !== true,
|
||||
};
|
||||
} else if (
|
||||
action.type === NavigationActions.PUSH &&
|
||||
@@ -321,7 +321,7 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
} else {
|
||||
return {
|
||||
...state,
|
||||
lastRouteKey: action.immediate !== true ? lastRouteKey : null,
|
||||
isTransitioning: action.immediate !== true,
|
||||
index: 0,
|
||||
routes: [state.routes[0]],
|
||||
};
|
||||
@@ -358,11 +358,11 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
if (
|
||||
action.type === NavigationActions.COMPLETE_TRANSITION &&
|
||||
(action.key == null || action.key === state.key) &&
|
||||
state.transitioningFromKey
|
||||
state.isTransitioning
|
||||
) {
|
||||
return {
|
||||
...state,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -441,7 +441,7 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
...state,
|
||||
routes: state.routes.slice(0, backRouteIndex),
|
||||
index: backRouteIndex - 1,
|
||||
transitioningFromKey: immediate !== true ? lastRouteKey : null,
|
||||
isTransitioning: immediate !== true,
|
||||
};
|
||||
} else if (
|
||||
backRouteIndex === 0 &&
|
||||
@@ -577,5 +577,7 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
routeConfigs,
|
||||
stackConfig.navigationOptions
|
||||
),
|
||||
|
||||
getScreenConfig: getScreenConfigDeprecated,
|
||||
};
|
||||
};
|
||||
|
||||
360
src/routers/SwitchRouter.js
Normal file
360
src/routers/SwitchRouter.js
Normal file
@@ -0,0 +1,360 @@
|
||||
import invariant from '../utils/invariant';
|
||||
import getScreenForRouteName from './getScreenForRouteName';
|
||||
import createConfigGetter from './createConfigGetter';
|
||||
|
||||
import NavigationActions from '../NavigationActions';
|
||||
import validateRouteConfigMap from './validateRouteConfigMap';
|
||||
import getScreenConfigDeprecated from './getScreenConfigDeprecated';
|
||||
|
||||
function childrenUpdateWithoutSwitchingIndex(actionType) {
|
||||
return [
|
||||
NavigationActions.SET_PARAMS,
|
||||
NavigationActions.COMPLETE_TRANSITION,
|
||||
].includes(actionType);
|
||||
}
|
||||
|
||||
export default (routeConfigs, config = {}) => {
|
||||
// Fail fast on invalid route definitions
|
||||
validateRouteConfigMap(routeConfigs);
|
||||
|
||||
const order = config.order || Object.keys(routeConfigs);
|
||||
const paths = config.paths || {};
|
||||
const initialRouteParams = config.initialRouteParams;
|
||||
const initialRouteName = config.initialRouteName || order[0];
|
||||
const backBehavior = config.backBehavior || 'none';
|
||||
const shouldBackNavigateToInitialRoute = backBehavior === 'initialRoute';
|
||||
const resetOnBlur = config.hasOwnProperty('resetOnBlur')
|
||||
? config.resetOnBlur
|
||||
: true;
|
||||
const initialRouteIndex = order.indexOf(initialRouteName);
|
||||
const childRouters = {};
|
||||
|
||||
order.forEach(routeName => {
|
||||
const routeConfig = routeConfigs[routeName];
|
||||
paths[routeName] =
|
||||
typeof routeConfig.path === 'string' ? routeConfig.path : routeName;
|
||||
childRouters[routeName] = null;
|
||||
const screen = getScreenForRouteName(routeConfigs, routeName);
|
||||
if (screen.router) {
|
||||
childRouters[routeName] = screen.router;
|
||||
}
|
||||
});
|
||||
if (initialRouteIndex === -1) {
|
||||
throw new Error(
|
||||
`Invalid initialRouteName '${initialRouteName}'.` +
|
||||
`Should be one of ${order.map(n => `"${n}"`).join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
function resetChildRoute(routeName) {
|
||||
const params =
|
||||
routeName === initialRouteName ? initialRouteParams : undefined;
|
||||
const childRouter = childRouters[routeName];
|
||||
if (childRouter) {
|
||||
const childAction = NavigationActions.init();
|
||||
return {
|
||||
...childRouter.getStateForAction(childAction),
|
||||
key: routeName,
|
||||
routeName,
|
||||
params,
|
||||
};
|
||||
}
|
||||
return {
|
||||
key: routeName,
|
||||
routeName,
|
||||
params,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
getInitialState() {
|
||||
const routes = order.map(resetChildRoute);
|
||||
return {
|
||||
routes,
|
||||
index: initialRouteIndex,
|
||||
isTransitioning: false,
|
||||
};
|
||||
},
|
||||
|
||||
getNextState(prevState, possibleNextState) {
|
||||
let nextState;
|
||||
if (prevState.index !== possibleNextState.index && resetOnBlur) {
|
||||
const prevRouteName = prevState.routes[prevState.index].routeName;
|
||||
const nextRoutes = [...possibleNextState.routes];
|
||||
nextRoutes[prevState.index] = resetChildRoute(prevRouteName);
|
||||
|
||||
return {
|
||||
...possibleNextState,
|
||||
routes: nextRoutes,
|
||||
};
|
||||
} else {
|
||||
nextState = possibleNextState;
|
||||
}
|
||||
|
||||
return nextState;
|
||||
},
|
||||
|
||||
getStateForAction(action, inputState) {
|
||||
let prevState = inputState ? { ...inputState } : inputState;
|
||||
let state = inputState || this.getInitialState();
|
||||
let activeChildIndex = state.index;
|
||||
|
||||
if (action.type === NavigationActions.INIT) {
|
||||
// NOTE(brentvatne): this seems weird... why are we merging these
|
||||
// params into child routes?
|
||||
// ---------------------------------------------------------------
|
||||
// Merge any params from the action into all the child routes
|
||||
const { params } = action;
|
||||
if (params) {
|
||||
state.routes = state.routes.map(route => ({
|
||||
...route,
|
||||
params: {
|
||||
...route.params,
|
||||
...params,
|
||||
...(route.routeName === initialRouteName
|
||||
? initialRouteParams
|
||||
: null),
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Let the current child handle it
|
||||
const activeChildLastState = state.routes[state.index];
|
||||
const activeChildRouter = childRouters[order[state.index]];
|
||||
if (activeChildRouter) {
|
||||
const activeChildState = activeChildRouter.getStateForAction(
|
||||
action,
|
||||
activeChildLastState
|
||||
);
|
||||
if (!activeChildState && inputState) {
|
||||
return null;
|
||||
}
|
||||
if (activeChildState && activeChildState !== activeChildLastState) {
|
||||
const routes = [...state.routes];
|
||||
routes[state.index] = activeChildState;
|
||||
return this.getNextState(prevState, {
|
||||
...state,
|
||||
routes,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Handle tab changing. Do this after letting the current tab try to
|
||||
// handle the action, to allow inner children to change first
|
||||
if (backBehavior !== 'none') {
|
||||
const isBackEligible =
|
||||
action.key == null || action.key === activeChildLastState.key;
|
||||
if (action.type === NavigationActions.BACK) {
|
||||
if (isBackEligible && shouldBackNavigateToInitialRoute) {
|
||||
activeChildIndex = initialRouteIndex;
|
||||
} else {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let didNavigate = false;
|
||||
if (action.type === NavigationActions.NAVIGATE) {
|
||||
const navigateAction = action;
|
||||
didNavigate = !!order.find((childId, i) => {
|
||||
if (childId === navigateAction.routeName) {
|
||||
activeChildIndex = i;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (didNavigate) {
|
||||
const childState = state.routes[activeChildIndex];
|
||||
let newChildState;
|
||||
|
||||
const childRouter = childRouters[action.routeName];
|
||||
|
||||
if (action.action) {
|
||||
newChildState = childRouter
|
||||
? childRouter.getStateForAction(action.action, childState)
|
||||
: null;
|
||||
} else if (!childRouter && action.params) {
|
||||
newChildState = {
|
||||
...childState,
|
||||
params: {
|
||||
...(childState.params || {}),
|
||||
...action.params,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (newChildState && newChildState !== childState) {
|
||||
const routes = [...state.routes];
|
||||
routes[activeChildIndex] = newChildState;
|
||||
return this.getNextState(prevState, {
|
||||
...state,
|
||||
routes,
|
||||
index: activeChildIndex,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (action.type === NavigationActions.SET_PARAMS) {
|
||||
const key = action.key;
|
||||
const lastRoute = state.routes.find(route => route.key === key);
|
||||
if (lastRoute) {
|
||||
const params = {
|
||||
...lastRoute.params,
|
||||
...action.params,
|
||||
};
|
||||
const routes = [...state.routes];
|
||||
routes[state.routes.indexOf(lastRoute)] = {
|
||||
...lastRoute,
|
||||
params,
|
||||
};
|
||||
return this.getNextState(prevState, {
|
||||
...state,
|
||||
routes,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (activeChildIndex !== state.index) {
|
||||
return this.getNextState(prevState, {
|
||||
...state,
|
||||
index: activeChildIndex,
|
||||
});
|
||||
} else if (didNavigate && !inputState) {
|
||||
return state;
|
||||
} else if (didNavigate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Let other children handle it and switch to the first child that returns a new state
|
||||
let index = state.index;
|
||||
let routes = state.routes;
|
||||
order.find((childId, i) => {
|
||||
const childRouter = childRouters[childId];
|
||||
if (i === index) {
|
||||
return false;
|
||||
}
|
||||
let childState = routes[i];
|
||||
if (childRouter) {
|
||||
childState = childRouter.getStateForAction(action, childState);
|
||||
}
|
||||
if (!childState) {
|
||||
index = i;
|
||||
return true;
|
||||
}
|
||||
if (childState !== routes[i]) {
|
||||
routes = [...routes];
|
||||
routes[i] = childState;
|
||||
index = i;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// Nested routers can be updated after switching children with actions such as SET_PARAMS
|
||||
// and COMPLETE_TRANSITION.
|
||||
// NOTE: This may be problematic with custom routers because we whitelist the actions
|
||||
// that can be handled by child routers without automatically changing index.
|
||||
if (childrenUpdateWithoutSwitchingIndex(action.type)) {
|
||||
index = state.index;
|
||||
}
|
||||
|
||||
if (index !== state.index || routes !== state.routes) {
|
||||
return this.getNextState(prevState, {
|
||||
...state,
|
||||
index,
|
||||
routes,
|
||||
});
|
||||
}
|
||||
return state;
|
||||
},
|
||||
|
||||
getComponentForState(state) {
|
||||
const routeName = state.routes[state.index].routeName;
|
||||
invariant(
|
||||
routeName,
|
||||
`There is no route defined for index ${state.index}. Check that
|
||||
that you passed in a navigation state with a valid tab/screen index.`
|
||||
);
|
||||
const childRouter = childRouters[routeName];
|
||||
if (childRouter) {
|
||||
return childRouter.getComponentForState(state.routes[state.index]);
|
||||
}
|
||||
return getScreenForRouteName(routeConfigs, routeName);
|
||||
},
|
||||
|
||||
getComponentForRouteName(routeName) {
|
||||
return getScreenForRouteName(routeConfigs, routeName);
|
||||
},
|
||||
|
||||
getPathAndParamsForState(state) {
|
||||
const route = state.routes[state.index];
|
||||
const routeName = order[state.index];
|
||||
const subPath = paths[routeName];
|
||||
const screen = getScreenForRouteName(routeConfigs, routeName);
|
||||
let path = subPath;
|
||||
let params = route.params;
|
||||
if (screen && screen.router) {
|
||||
const stateRoute = route;
|
||||
// If it has a router it's a navigator.
|
||||
// If it doesn't have router it's an ordinary React component.
|
||||
const child = screen.router.getPathAndParamsForState(stateRoute);
|
||||
path = subPath ? `${subPath}/${child.path}` : child.path;
|
||||
params = child.params ? { ...params, ...child.params } : params;
|
||||
}
|
||||
return {
|
||||
path,
|
||||
params,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets an optional action, based on a relative path and query params.
|
||||
*
|
||||
* This will return null if there is no action matched
|
||||
*/
|
||||
getActionForPathAndParams(path, params) {
|
||||
return (
|
||||
order
|
||||
.map(childId => {
|
||||
const parts = path.split('/');
|
||||
const pathToTest = paths[childId];
|
||||
if (parts[0] === pathToTest) {
|
||||
const childRouter = childRouters[childId];
|
||||
const action = NavigationActions.navigate({
|
||||
routeName: childId,
|
||||
});
|
||||
if (childRouter && childRouter.getActionForPathAndParams) {
|
||||
action.action = childRouter.getActionForPathAndParams(
|
||||
parts.slice(1).join('/'),
|
||||
params
|
||||
);
|
||||
} else if (params) {
|
||||
action.params = params;
|
||||
}
|
||||
return action;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.find(action => !!action) ||
|
||||
order
|
||||
.map(childId => {
|
||||
const childRouter = childRouters[childId];
|
||||
return (
|
||||
childRouter && childRouter.getActionForPathAndParams(path, params)
|
||||
);
|
||||
})
|
||||
.find(action => !!action) ||
|
||||
null
|
||||
);
|
||||
},
|
||||
|
||||
getScreenOptions: createConfigGetter(
|
||||
routeConfigs,
|
||||
config.navigationOptions
|
||||
),
|
||||
|
||||
getScreenConfig: getScreenConfigDeprecated,
|
||||
};
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import createConfigGetter from './createConfigGetter';
|
||||
|
||||
import NavigationActions from '../NavigationActions';
|
||||
import validateRouteConfigMap from './validateRouteConfigMap';
|
||||
import getScreenConfigDeprecated from './getScreenConfigDeprecated';
|
||||
|
||||
function childrenUpdateWithoutSwitchingIndex(actionType) {
|
||||
return [
|
||||
@@ -67,7 +68,7 @@ export default (routeConfigs, config = {}) => {
|
||||
state = {
|
||||
routes,
|
||||
index: initialRouteIndex,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
};
|
||||
// console.log(`${order.join('-')}: Initial state`, {state});
|
||||
}
|
||||
@@ -322,5 +323,7 @@ export default (routeConfigs, config = {}) => {
|
||||
routeConfigs,
|
||||
config.navigationOptions
|
||||
),
|
||||
|
||||
getScreenConfig: getScreenConfigDeprecated,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
/* 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,
|
||||
transitioningFromKey: false,
|
||||
isTransitioning: false,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
|
||||
@@ -92,7 +92,7 @@ describe('StackRouter', () => {
|
||||
expect(
|
||||
router.getComponentForState({
|
||||
index: 0,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
routes: [
|
||||
{ key: 'a', routeName: 'foo' },
|
||||
{ key: 'b', routeName: 'bar' },
|
||||
@@ -103,7 +103,7 @@ describe('StackRouter', () => {
|
||||
expect(
|
||||
router.getComponentForState({
|
||||
index: 1,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
routes: [
|
||||
{ key: 'a', routeName: 'foo' },
|
||||
{ key: 'b', routeName: 'bar' },
|
||||
@@ -127,7 +127,7 @@ describe('StackRouter', () => {
|
||||
expect(
|
||||
router.getComponentForState({
|
||||
index: 0,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
routes: [
|
||||
{ key: 'a', routeName: 'foo' },
|
||||
{ key: 'b', routeName: 'bar' },
|
||||
@@ -138,7 +138,7 @@ describe('StackRouter', () => {
|
||||
expect(
|
||||
router.getComponentForState({
|
||||
index: 1,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
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,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [{ key: 'id-0', routeName: 'foo' }],
|
||||
});
|
||||
@@ -494,7 +494,7 @@ describe('StackRouter', () => {
|
||||
|
||||
const state = {
|
||||
index: 2,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
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.transitioningFromKey).toBe('C');
|
||||
expect(poppedState.isTransitioning).toBe(true);
|
||||
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.transitioningFromKey).toBe(null);
|
||||
expect(poppedImmediatelyState.isTransitioning).toBe(false);
|
||||
});
|
||||
|
||||
test('Navigate Pushes duplicate routeName', () => {
|
||||
@@ -678,7 +678,7 @@ describe('StackRouter', () => {
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
expect(state).toEqual({
|
||||
index: 0,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
@@ -706,7 +706,7 @@ describe('StackRouter', () => {
|
||||
);
|
||||
expect(state3).toEqual({
|
||||
index: 0,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
@@ -773,7 +773,7 @@ describe('StackRouter', () => {
|
||||
state
|
||||
);
|
||||
expect(state2 && state2.index).toEqual(1);
|
||||
expect(state2 && state2.transitioningFromKey).toEqual(state.routes[0].key);
|
||||
expect(state2 && state2.isTransitioning).toEqual(true);
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.COMPLETE_TRANSITION,
|
||||
@@ -781,7 +781,7 @@ describe('StackRouter', () => {
|
||||
state2
|
||||
);
|
||||
expect(state3 && state3.index).toEqual(1);
|
||||
expect(state3 && state3.transitioningFromKey).toEqual(null);
|
||||
expect(state3 && state3.isTransitioning).toEqual(false);
|
||||
});
|
||||
|
||||
test('Handle basic stack logic for components with router', () => {
|
||||
@@ -803,7 +803,7 @@ describe('StackRouter', () => {
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
expect(state).toEqual({
|
||||
index: 0,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
@@ -831,7 +831,7 @@ describe('StackRouter', () => {
|
||||
);
|
||||
expect(state3).toEqual({
|
||||
index: 0,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
@@ -905,7 +905,7 @@ describe('StackRouter', () => {
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
expect(state).toEqual({
|
||||
index: 0,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
@@ -929,7 +929,7 @@ describe('StackRouter', () => {
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
expect(state).toEqual({
|
||||
index: 0,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
@@ -1274,19 +1274,19 @@ describe('StackRouter', () => {
|
||||
|
||||
expect(state).toEqual({
|
||||
index: 0,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
index: 0,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
key: 'id-2',
|
||||
params: { code: 'test', foo: 'bar' },
|
||||
routeName: 'main',
|
||||
routes: [
|
||||
{
|
||||
index: 0,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
key: 'id-1',
|
||||
params: { code: 'test', foo: 'bar', id: '4' },
|
||||
routeName: 'profile',
|
||||
@@ -1333,19 +1333,19 @@ describe('StackRouter', () => {
|
||||
|
||||
expect(state2).toEqual({
|
||||
index: 0,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
index: 0,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
key: 'id-5',
|
||||
params: { code: '', foo: 'bar' },
|
||||
routeName: 'main',
|
||||
routes: [
|
||||
{
|
||||
index: 0,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
key: 'id-4',
|
||||
params: { code: '', foo: 'bar', id: '4' },
|
||||
routeName: 'profile',
|
||||
@@ -1448,7 +1448,7 @@ describe('StackRouter', () => {
|
||||
|
||||
const state = {
|
||||
index: 0,
|
||||
transitioningFromKey: null,
|
||||
isTransitioning: false,
|
||||
routes: [
|
||||
{
|
||||
index: 1,
|
||||
@@ -1664,12 +1664,10 @@ test('Handles deep navigate completion action', () => {
|
||||
},
|
||||
state
|
||||
);
|
||||
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(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(!!key).toEqual(true);
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
@@ -1677,8 +1675,8 @@ test('Handles deep navigate completion action', () => {
|
||||
},
|
||||
state2
|
||||
);
|
||||
expect(state3.index).toEqual(0);
|
||||
expect(state3.transitioningFromKey).toEqual(null);
|
||||
expect(state3.routes[0].index).toEqual(1);
|
||||
expect(state3.routes[0].transitioningFromKey).toEqual(null);
|
||||
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);
|
||||
});
|
||||
|
||||
109
src/routers/__tests__/SwitchRouter-test.js
Normal file
109
src/routers/__tests__/SwitchRouter-test.js
Normal file
@@ -0,0 +1,109 @@
|
||||
/* eslint react/display-name:0 */
|
||||
|
||||
import React from 'react';
|
||||
import SwitchRouter from '../SwitchRouter';
|
||||
import StackRouter from '../StackRouter';
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
|
||||
describe('SwitchRouter', () => {
|
||||
test('resets the route when unfocusing a tab by default', () => {
|
||||
const router = getExampleRouter();
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
const state2 = router.getStateForAction(
|
||||
{ type: NavigationActions.NAVIGATE, routeName: 'A2' },
|
||||
state
|
||||
);
|
||||
expect(state2.routes[0].index).toEqual(1);
|
||||
expect(state2.routes[0].routes.length).toEqual(2);
|
||||
|
||||
const state3 = router.getStateForAction(
|
||||
{ type: NavigationActions.NAVIGATE, routeName: 'B' },
|
||||
state2
|
||||
);
|
||||
|
||||
expect(state3.routes[0].index).toEqual(0);
|
||||
expect(state3.routes[0].routes.length).toEqual(1);
|
||||
});
|
||||
|
||||
test('does not reset the route on unfocus if resetOnBlur is false', () => {
|
||||
const router = getExampleRouter({ resetOnBlur: false });
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
const state2 = router.getStateForAction(
|
||||
{ type: NavigationActions.NAVIGATE, routeName: 'A2' },
|
||||
state
|
||||
);
|
||||
expect(state2.routes[0].index).toEqual(1);
|
||||
expect(state2.routes[0].routes.length).toEqual(2);
|
||||
|
||||
const state3 = router.getStateForAction(
|
||||
{ type: NavigationActions.NAVIGATE, routeName: 'B' },
|
||||
state2
|
||||
);
|
||||
|
||||
expect(state3.routes[0].index).toEqual(1);
|
||||
expect(state3.routes[0].routes.length).toEqual(2);
|
||||
});
|
||||
|
||||
test('ignores back by default', () => {
|
||||
const router = getExampleRouter();
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
const state2 = router.getStateForAction(
|
||||
{ type: NavigationActions.NAVIGATE, routeName: 'B' },
|
||||
state
|
||||
);
|
||||
expect(state2.index).toEqual(1);
|
||||
|
||||
const state3 = router.getStateForAction(
|
||||
{ type: NavigationActions.BACK },
|
||||
state2
|
||||
);
|
||||
|
||||
expect(state3.index).toEqual(1);
|
||||
});
|
||||
|
||||
test('handles back if given a backBehavior', () => {
|
||||
const router = getExampleRouter({ backBehavior: 'initialRoute' });
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
const state2 = router.getStateForAction(
|
||||
{ type: NavigationActions.NAVIGATE, routeName: 'B' },
|
||||
state
|
||||
);
|
||||
expect(state2.index).toEqual(1);
|
||||
|
||||
const state3 = router.getStateForAction(
|
||||
{ type: NavigationActions.BACK },
|
||||
state2
|
||||
);
|
||||
|
||||
expect(state3.index).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
const getExampleRouter = (config = {}) => {
|
||||
const PlainScreen = () => <div />;
|
||||
const StackA = () => <div />;
|
||||
const StackB = () => <div />;
|
||||
|
||||
StackA.router = StackRouter({
|
||||
A1: PlainScreen,
|
||||
A2: PlainScreen,
|
||||
});
|
||||
|
||||
StackB.router = StackRouter({
|
||||
B1: PlainScreen,
|
||||
B2: PlainScreen,
|
||||
});
|
||||
|
||||
const router = SwitchRouter(
|
||||
{
|
||||
A: StackA,
|
||||
B: StackB,
|
||||
},
|
||||
{
|
||||
initialRouteName: 'A',
|
||||
...config,
|
||||
}
|
||||
);
|
||||
|
||||
return router;
|
||||
};
|
||||
@@ -1,7 +1,9 @@
|
||||
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') {
|
||||
@@ -36,6 +38,28 @@ 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;
|
||||
@@ -43,7 +67,11 @@ export default (routeConfigs, navigatorScreenConfig) => (
|
||||
|
||||
const configOptions = { navigation, screenProps: screenProps || {} };
|
||||
|
||||
let outputConfig = applyConfig(navigatorScreenConfig, {}, configOptions);
|
||||
outputConfig = applyConfig(
|
||||
navigatorScreenConfig,
|
||||
outputConfig,
|
||||
configOptions
|
||||
);
|
||||
outputConfig = applyConfig(
|
||||
componentScreenConfig,
|
||||
outputConfig,
|
||||
|
||||
7
src/routers/getScreenConfigDeprecated.js
Normal file
7
src/routers/getScreenConfigDeprecated.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import invariant from '../utils/invariant';
|
||||
|
||||
export default () =>
|
||||
invariant(
|
||||
false,
|
||||
'`getScreenConfig` has been replaced with `getScreenOptions`'
|
||||
);
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { Animated, StyleSheet } from 'react-native';
|
||||
import createPointerEventsContainer from './createPointerEventsContainer';
|
||||
import createPointerEventsContainer from './PointerEventsContainer';
|
||||
|
||||
/**
|
||||
* Component that renders the scene as card for the <StackView />.
|
||||
* Component that renders the scene as card for the <NavigationCardStack />.
|
||||
*/
|
||||
class Card extends React.Component {
|
||||
render() {
|
||||
@@ -22,12 +22,16 @@ class Card extends React.Component {
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
main: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: '#E9E9EF',
|
||||
backgroundColor: '#EFEFF4',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
shadowColor: 'black',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 5,
|
||||
top: 0,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import clamp from 'clamp';
|
||||
import {
|
||||
@@ -11,12 +11,14 @@ import {
|
||||
Easing,
|
||||
} from 'react-native';
|
||||
|
||||
import Card from './StackViewCard';
|
||||
import Card from './Card';
|
||||
import Header from '../Header/Header';
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
import addNavigationHelpers from '../../addNavigationHelpers';
|
||||
import getChildEventSubscriber from '../../getChildEventSubscriber';
|
||||
import SceneView from '../SceneView';
|
||||
|
||||
import TransitionConfigs from './StackViewTransitionConfigs';
|
||||
import TransitionConfigs from './TransitionConfigs';
|
||||
import * as ReactNativeFeatures from '../../utils/ReactNativeFeatures';
|
||||
|
||||
const emptyFunction = () => {};
|
||||
@@ -57,7 +59,7 @@ const animatedSubscribeValue = animatedValue => {
|
||||
}
|
||||
};
|
||||
|
||||
class StackViewLayout extends React.Component {
|
||||
class CardStack extends React.Component {
|
||||
/**
|
||||
* Used to identify the starting point of the position when the gesture starts, such that it can
|
||||
* be updated according to its relative position. This means that a card can effectively be
|
||||
@@ -78,15 +80,76 @@ class StackViewLayout extends React.Component {
|
||||
*/
|
||||
_immediateIndex = null;
|
||||
|
||||
_screenDetails = {};
|
||||
|
||||
_childEventSubscribers = {};
|
||||
|
||||
componentWillReceiveProps(props) {
|
||||
if (props.screenProps !== this.props.screenProps) {
|
||||
this._screenDetails = {};
|
||||
}
|
||||
props.transitionProps.scenes.forEach(newScene => {
|
||||
if (
|
||||
this._screenDetails[newScene.key] &&
|
||||
this._screenDetails[newScene.key].state !== newScene.route
|
||||
) {
|
||||
this._screenDetails[newScene.key] = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const activeKeys = this.props.transitionProps.navigation.state.routes.map(
|
||||
route => route.key
|
||||
);
|
||||
Object.keys(this._childEventSubscribers).forEach(key => {
|
||||
if (!activeKeys.includes(key)) {
|
||||
delete this._childEventSubscribers[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_isRouteFocused = route => {
|
||||
const { transitionProps: { navigation: { state } } } = this.props;
|
||||
const focusedRoute = state.routes[state.index];
|
||||
return route === focusedRoute;
|
||||
};
|
||||
|
||||
_getScreenDetails = scene => {
|
||||
const { screenProps, transitionProps: { navigation }, router } = this.props;
|
||||
let screenDetails = this._screenDetails[scene.key];
|
||||
if (!screenDetails || screenDetails.state !== scene.route) {
|
||||
if (!this._childEventSubscribers[scene.route.key]) {
|
||||
this._childEventSubscribers[scene.route.key] = getChildEventSubscriber(
|
||||
navigation.addListener,
|
||||
scene.route.key
|
||||
);
|
||||
}
|
||||
|
||||
const screenNavigation = addNavigationHelpers({
|
||||
dispatch: navigation.dispatch,
|
||||
state: scene.route,
|
||||
isFocused: () => this._isRouteFocused(scene.route),
|
||||
addListener: this._childEventSubscribers[scene.route.key],
|
||||
});
|
||||
screenDetails = {
|
||||
state: scene.route,
|
||||
navigation: screenNavigation,
|
||||
options: router.getScreenOptions(screenNavigation, screenProps),
|
||||
};
|
||||
this._screenDetails[scene.key] = screenDetails;
|
||||
}
|
||||
return screenDetails;
|
||||
};
|
||||
|
||||
_renderHeader(scene, headerMode) {
|
||||
const { options } = scene.descriptor;
|
||||
const { header } = options;
|
||||
const { header } = this._getScreenDetails(scene).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,
|
||||
@@ -106,6 +169,7 @@ class StackViewLayout extends React.Component {
|
||||
scene,
|
||||
mode: headerMode,
|
||||
transitionPreset: this._getHeaderTransitionPreset(),
|
||||
getScreenDetails: this._getScreenDetails,
|
||||
leftInterpolator: headerLeftInterpolator,
|
||||
titleInterpolator: headerTitleInterpolator,
|
||||
rightInterpolator: headerRightInterpolator,
|
||||
@@ -205,8 +269,7 @@ class StackViewLayout extends React.Component {
|
||||
} = this.props;
|
||||
const { index } = navigation.state;
|
||||
const isVertical = mode === 'modal';
|
||||
const { options } = scene.descriptor;
|
||||
|
||||
const { options } = this._getScreenDetails(scene);
|
||||
const gestureDirectionInverted = options.gestureDirection === 'inverted';
|
||||
|
||||
const gesturesEnabled =
|
||||
@@ -222,7 +285,7 @@ class StackViewLayout extends React.Component {
|
||||
this._reset(index, 0);
|
||||
},
|
||||
onPanResponderGrant: () => {
|
||||
position.stopAnimation((value: number) => {
|
||||
position.stopAnimation(value => {
|
||||
this._isResponding = true;
|
||||
this._gestureStartValue = value;
|
||||
});
|
||||
@@ -246,12 +309,9 @@ class StackViewLayout 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 = {},
|
||||
} = options;
|
||||
} = this._getScreenDetails(scene).options;
|
||||
const gestureResponseDistance = isVertical
|
||||
? userGestureResponseDistance.vertical ||
|
||||
GESTURE_RESPONSE_DISTANCE_VERTICAL
|
||||
@@ -352,7 +412,7 @@ class StackViewLayout 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>
|
||||
@@ -384,10 +444,8 @@ class StackViewLayout extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
_renderInnerScene(scene) {
|
||||
const { options, navigation, getComponent } = scene.descriptor;
|
||||
const SceneComponent = getComponent();
|
||||
|
||||
_renderInnerScene(SceneComponent, scene) {
|
||||
const { navigation } = this._getScreenDetails(scene);
|
||||
const { screenProps } = this.props;
|
||||
const headerMode = this._getHeaderMode();
|
||||
if (headerMode === 'screen') {
|
||||
@@ -406,7 +464,7 @@ class StackViewLayout extends React.Component {
|
||||
}
|
||||
return (
|
||||
<SceneView
|
||||
screenProps={screenProps}
|
||||
screenProps={this.props.screenProps}
|
||||
navigation={navigation}
|
||||
component={SceneComponent}
|
||||
/>
|
||||
@@ -430,14 +488,21 @@ class StackViewLayout 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
|
||||
{...this.props.transitionProps}
|
||||
{...props}
|
||||
{...transitionProps}
|
||||
key={`card_${scene.key}`}
|
||||
style={[style, this.props.cardStyle]}
|
||||
scene={scene}
|
||||
>
|
||||
{this._renderInnerScene(scene)}
|
||||
{this._renderInnerScene(SceneComponent, scene)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -457,4 +522,4 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
export default StackViewLayout;
|
||||
export default CardStack;
|
||||
@@ -159,9 +159,17 @@ 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,
|
||||
};
|
||||
82
src/views/CardStack/CardStackTransitioner.js
Normal file
82
src/views/CardStack/CardStackTransitioner.js
Normal file
@@ -0,0 +1,82 @@
|
||||
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;
|
||||
95
src/views/CardStack/PointerEventsContainer.js
Normal file
95
src/views/CardStack/PointerEventsContainer.js
Normal file
@@ -0,0 +1,95 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Animated, Easing, Platform } from 'react-native';
|
||||
import StyleInterpolator from './StackViewStyleInterpolator';
|
||||
import CardStackStyleInterpolator from './CardStackStyleInterpolator';
|
||||
import * as ReactNativeFeatures from '../../utils/ReactNativeFeatures';
|
||||
|
||||
let IOSTransitionSpec;
|
||||
@@ -23,7 +23,7 @@ if (ReactNativeFeatures.supportsImprovedSpringAnimation()) {
|
||||
// Standard iOS navigation transition
|
||||
const SlideFromRightIOS = {
|
||||
transitionSpec: IOSTransitionSpec,
|
||||
screenInterpolator: StyleInterpolator.forHorizontal,
|
||||
screenInterpolator: CardStackStyleInterpolator.forHorizontal,
|
||||
containerStyle: {
|
||||
backgroundColor: '#000',
|
||||
},
|
||||
@@ -32,7 +32,7 @@ const SlideFromRightIOS = {
|
||||
// Standard iOS navigation transition for modals
|
||||
const ModalSlideFromBottomIOS = {
|
||||
transitionSpec: IOSTransitionSpec,
|
||||
screenInterpolator: StyleInterpolator.forVertical,
|
||||
screenInterpolator: CardStackStyleInterpolator.forVertical,
|
||||
containerStyle: {
|
||||
backgroundColor: '#000',
|
||||
},
|
||||
@@ -46,7 +46,7 @@ const FadeInFromBottomAndroid = {
|
||||
easing: Easing.out(Easing.poly(5)), // decelerate
|
||||
timing: Animated.timing,
|
||||
},
|
||||
screenInterpolator: StyleInterpolator.forFadeFromBottomAndroid,
|
||||
screenInterpolator: CardStackStyleInterpolator.forFadeFromBottomAndroid,
|
||||
};
|
||||
|
||||
// Standard Android navigation transition when closing an Activity
|
||||
@@ -57,22 +57,27 @@ const FadeOutToBottomAndroid = {
|
||||
easing: Easing.in(Easing.poly(4)), // accelerate
|
||||
timing: Animated.timing,
|
||||
},
|
||||
screenInterpolator: StyleInterpolator.forFadeFromBottomAndroid,
|
||||
screenInterpolator: CardStackStyleInterpolator.forFadeFromBottomAndroid,
|
||||
};
|
||||
|
||||
function defaultTransitionConfig(transitionProps, isModal) {
|
||||
function defaultTransitionConfig(
|
||||
// props for the new screen
|
||||
transitionProps,
|
||||
// props for the old screen
|
||||
prevTransitionProps,
|
||||
// whether we're animating in/out a modal screen
|
||||
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;
|
||||
// }
|
||||
// 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
|
||||
@@ -82,12 +87,23 @@ function defaultTransitionConfig(transitionProps, isModal) {
|
||||
return SlideFromRightIOS;
|
||||
}
|
||||
|
||||
function getTransitionConfig(transitionConfigurer, transitionProps, isModal) {
|
||||
const defaultConfig = defaultTransitionConfig(transitionProps, isModal);
|
||||
function getTransitionConfig(
|
||||
transitionConfigurer,
|
||||
// props for the new screen
|
||||
transitionProps,
|
||||
// props for the old screen
|
||||
prevTransitionProps,
|
||||
isModal
|
||||
) {
|
||||
const defaultConfig = defaultTransitionConfig(
|
||||
transitionProps,
|
||||
prevTransitionProps,
|
||||
isModal
|
||||
);
|
||||
if (transitionConfigurer) {
|
||||
return {
|
||||
...defaultConfig,
|
||||
...transitionConfigurer(transitionProps, isModal),
|
||||
...transitionConfigurer(transitionProps, prevTransitionProps, isModal),
|
||||
};
|
||||
}
|
||||
return defaultConfig;
|
||||
@@ -1,24 +1,30 @@
|
||||
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 { descriptors, navigation, screenProps } = this.props;
|
||||
const {
|
||||
router,
|
||||
navigation,
|
||||
childNavigationProps,
|
||||
screenProps,
|
||||
} = this.props;
|
||||
const { routes, index } = navigation.state;
|
||||
const descriptor = descriptors[routes[index].key];
|
||||
const Content = descriptor.getComponent();
|
||||
const childNavigation = childNavigationProps[routes[index].key];
|
||||
const Content = router.getComponentForRouteName(routes[index].routeName);
|
||||
return (
|
||||
<SceneView
|
||||
screenProps={screenProps}
|
||||
component={Content}
|
||||
navigation={descriptor.navigation}
|
||||
navigation={childNavigation}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DrawerScreen;
|
||||
export default withCachedChildNavigation(DrawerScreen);
|
||||
|
||||
@@ -2,21 +2,32 @@ 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 descriptor = this.props.descriptors[routeKey];
|
||||
invariant(
|
||||
descriptor.options,
|
||||
'Cannot access screen descriptor options from drawer sidebar'
|
||||
const DrawerScreen = this.props.router.getComponentForRouteName(
|
||||
'DrawerClose'
|
||||
);
|
||||
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
|
||||
);
|
||||
return descriptor.options;
|
||||
};
|
||||
|
||||
_getLabel = ({ focused, tintColor, route }) => {
|
||||
@@ -45,6 +56,7 @@ 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)
|
||||
@@ -74,7 +86,6 @@ 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
|
||||
@@ -83,6 +94,7 @@ class DrawerSidebar extends React.PureComponent {
|
||||
getLabel={this._getLabel}
|
||||
renderIcon={this._renderIcon}
|
||||
onItemPress={this._onItemPress}
|
||||
router={this.props.router}
|
||||
drawerPosition={this.props.drawerPosition}
|
||||
/>
|
||||
</View>
|
||||
@@ -90,7 +102,7 @@ class DrawerSidebar extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
export default DrawerSidebar;
|
||||
export default withCachedChildNavigation(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 NavigationActions from '../../NavigationActions';
|
||||
import getChildEventSubscriber from '../../getChildEventSubscriber';
|
||||
|
||||
/**
|
||||
* Component that renders the drawer.
|
||||
@@ -12,12 +12,16 @@ import NavigationActions from '../../NavigationActions';
|
||||
export default class DrawerView extends React.PureComponent {
|
||||
state = {
|
||||
drawerWidth:
|
||||
typeof this.props.navigationConfig.drawerWidth === 'function'
|
||||
? this.props.navigationConfig.drawerWidth()
|
||||
: this.props.navigationConfig.drawerWidth,
|
||||
typeof this.props.drawerWidth === 'function'
|
||||
? this.props.drawerWidth()
|
||||
: this.props.drawerWidth,
|
||||
};
|
||||
|
||||
_childEventSubscribers = {};
|
||||
|
||||
componentWillMount() {
|
||||
this._updateScreenNavigation(this.props.navigation);
|
||||
|
||||
Dimensions.addEventListener('change', this._updateWidth);
|
||||
}
|
||||
|
||||
@@ -25,60 +29,130 @@ export default class DrawerView extends React.PureComponent {
|
||||
Dimensions.removeEventListener('change', this._updateWidth);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const activeKeys = this.props.navigation.state.routes.map(
|
||||
route => route.key
|
||||
);
|
||||
Object.keys(this._childEventSubscribers).forEach(key => {
|
||||
if (!activeKeys.includes(key)) {
|
||||
delete this._childEventSubscribers[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
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();
|
||||
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();
|
||||
}
|
||||
}
|
||||
this._updateScreenNavigation(nextProps.navigation);
|
||||
}
|
||||
|
||||
_handleDrawerOpen = () => {
|
||||
const { navigation } = this.props;
|
||||
navigation.dispatch({ type: NavigationActions.OPEN_DRAWER });
|
||||
const { navigation, drawerOpenRoute } = this.props;
|
||||
const { routes, index } = navigation.state;
|
||||
if (routes[index].routeName !== drawerOpenRoute) {
|
||||
this.props.navigation.navigate(drawerOpenRoute);
|
||||
}
|
||||
};
|
||||
|
||||
_handleDrawerClose = () => {
|
||||
const { navigation } = this.props;
|
||||
navigation.dispatch({ type: NavigationActions.CLOSE_DRAWER });
|
||||
const { navigation, drawerCloseRoute } = this.props;
|
||||
const { routes, index } = navigation.state;
|
||||
if (routes[index].routeName !== drawerCloseRoute) {
|
||||
this.props.navigation.navigate(drawerCloseRoute);
|
||||
}
|
||||
};
|
||||
|
||||
_isRouteFocused = route => () => {
|
||||
const { state } = this.props.navigation;
|
||||
const focusedRoute = state.routes[state.index];
|
||||
return route === focusedRoute;
|
||||
};
|
||||
|
||||
_updateScreenNavigation = navigation => {
|
||||
const { drawerCloseRoute } = this.props;
|
||||
const navigationState = navigation.state.routes.find(
|
||||
route => route.routeName === drawerCloseRoute
|
||||
);
|
||||
if (
|
||||
this._screenNavigationProp &&
|
||||
this._screenNavigationProp.state === navigationState
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._childEventSubscribers[navigationState.key]) {
|
||||
this._childEventSubscribers[
|
||||
navigationState.key
|
||||
] = getChildEventSubscriber(navigation.addListener, navigationState.key);
|
||||
}
|
||||
|
||||
this._screenNavigationProp = addNavigationHelpers({
|
||||
dispatch: navigation.dispatch,
|
||||
state: navigationState,
|
||||
isFocused: () => this._isRouteFocused(navigationState),
|
||||
addListener: this._childEventSubscribers[navigationState.key],
|
||||
});
|
||||
};
|
||||
|
||||
_updateWidth = () => {
|
||||
const drawerWidth =
|
||||
typeof this.props.navigationConfig.drawerWidth === 'function'
|
||||
? this.props.navigationConfig.drawerWidth()
|
||||
: this.props.navigationConfig.drawerWidth;
|
||||
typeof this.props.drawerWidth === 'function'
|
||||
? this.props.drawerWidth()
|
||||
: this.props.drawerWidth;
|
||||
|
||||
if (this.state.drawerWidth !== drawerWidth) {
|
||||
this.setState({ drawerWidth });
|
||||
}
|
||||
};
|
||||
|
||||
_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}
|
||||
/>
|
||||
_getNavigationState = navigation => {
|
||||
const { drawerCloseRoute } = this.props;
|
||||
const navigationState = navigation.state.routes.find(
|
||||
route => route.routeName === drawerCloseRoute
|
||||
);
|
||||
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 { state } = this.props.navigation;
|
||||
const activeKey = state.routes[state.index].key;
|
||||
const descriptor = this.props.descriptors[activeKey];
|
||||
const DrawerScreen = this.props.router.getComponentForRouteName(
|
||||
this.props.drawerCloseRoute
|
||||
);
|
||||
|
||||
const DrawerScreen = descriptor.getComponent();
|
||||
|
||||
const { drawerLockMode } = descriptor.options;
|
||||
const config = this.props.router.getScreenOptions(
|
||||
this._screenNavigationProp,
|
||||
this.props.screenProps
|
||||
);
|
||||
|
||||
return (
|
||||
<DrawerLayout
|
||||
@@ -87,25 +161,23 @@ export default class DrawerView extends React.PureComponent {
|
||||
}}
|
||||
drawerLockMode={
|
||||
(this.props.screenProps && this.props.screenProps.drawerLockMode) ||
|
||||
this.props.navigationConfig.drawerLockMode
|
||||
}
|
||||
drawerBackgroundColor={
|
||||
this.props.navigationConfig.drawerBackgroundColor
|
||||
(config && config.drawerLockMode)
|
||||
}
|
||||
drawerBackgroundColor={this.props.drawerBackgroundColor}
|
||||
drawerWidth={this.state.drawerWidth}
|
||||
onDrawerOpen={this._handleDrawerOpen}
|
||||
onDrawerClose={this._handleDrawerClose}
|
||||
useNativeAnimations={this.props.navigationConfig.useNativeAnimations}
|
||||
useNativeAnimations={this.props.useNativeAnimations}
|
||||
renderNavigationView={this._renderNavigationView}
|
||||
drawerPosition={
|
||||
this.props.navigationConfig.drawerPosition === 'right'
|
||||
this.props.drawerPosition === 'right'
|
||||
? DrawerLayout.positions.Right
|
||||
: DrawerLayout.positions.Left
|
||||
}
|
||||
>
|
||||
<DrawerScreen
|
||||
screenProps={this.props.screenProps}
|
||||
navigation={descriptor.navigation}
|
||||
navigation={this._screenNavigationProp}
|
||||
/>
|
||||
</DrawerLayout>
|
||||
);
|
||||
|
||||
@@ -47,11 +47,11 @@ class Header extends React.PureComponent {
|
||||
};
|
||||
|
||||
_getHeaderTitleString(scene) {
|
||||
const options = scene.descriptor.options;
|
||||
if (typeof options.headerTitle === 'string') {
|
||||
return options.headerTitle;
|
||||
const sceneOptions = this.props.getScreenDetails(scene).options;
|
||||
if (typeof sceneOptions.headerTitle === 'string') {
|
||||
return sceneOptions.headerTitle;
|
||||
}
|
||||
return options.title;
|
||||
return sceneOptions.title;
|
||||
}
|
||||
|
||||
_getLastScene(scene) {
|
||||
@@ -63,7 +63,7 @@ class Header extends React.PureComponent {
|
||||
if (!lastScene) {
|
||||
return null;
|
||||
}
|
||||
const { headerBackTitle } = lastScene.descriptor.options;
|
||||
const { headerBackTitle } = this.props.getScreenDetails(lastScene).options;
|
||||
if (headerBackTitle || headerBackTitle === null) {
|
||||
return headerBackTitle;
|
||||
}
|
||||
@@ -75,20 +75,27 @@ class Header extends React.PureComponent {
|
||||
if (!lastScene) {
|
||||
return null;
|
||||
}
|
||||
return lastScene.descriptor.options.headerTruncatedBackTitle;
|
||||
return this.props.getScreenDetails(lastScene).options
|
||||
.headerTruncatedBackTitle;
|
||||
}
|
||||
|
||||
_navigateBack = () => {
|
||||
requestAnimationFrame(() => {
|
||||
this.props.navigation.goBack(this.props.scene.route.key);
|
||||
});
|
||||
};
|
||||
|
||||
_renderTitleComponent = props => {
|
||||
const { options } = props.scene.descriptor;
|
||||
const headerTitle = options.headerTitle;
|
||||
const details = this.props.getScreenDetails(props.scene);
|
||||
const headerTitle = details.options.headerTitle;
|
||||
if (React.isValidElement(headerTitle)) {
|
||||
return headerTitle;
|
||||
}
|
||||
const titleString = this._getHeaderTitleString(props.scene);
|
||||
|
||||
const titleStyle = options.headerTitleStyle;
|
||||
const color = options.headerTintColor;
|
||||
const allowFontScaling = options.headerTitleAllowFontScaling;
|
||||
const titleStyle = details.options.headerTitleStyle;
|
||||
const color = details.options.headerTintColor;
|
||||
const allowFontScaling = details.options.headerTitleAllowFontScaling;
|
||||
|
||||
// On iOS, width of left/right components depends on the calculated
|
||||
// size of the title.
|
||||
@@ -120,7 +127,8 @@ class Header extends React.PureComponent {
|
||||
};
|
||||
|
||||
_renderLeftComponent = props => {
|
||||
const { options } = props.scene.descriptor;
|
||||
const { options } = this.props.getScreenDetails(props.scene);
|
||||
|
||||
if (
|
||||
React.isValidElement(options.headerLeft) ||
|
||||
options.headerLeft === null
|
||||
@@ -140,15 +148,9 @@ class Header extends React.PureComponent {
|
||||
? (this.props.layout.initWidth - this.state.widths[props.scene.key]) / 2
|
||||
: undefined;
|
||||
const RenderedLeftComponent = options.headerLeft || HeaderBackButton;
|
||||
const goBack = () => {
|
||||
// Go back on next tick because button ripple effect needs to happen on Android
|
||||
requestAnimationFrame(() => {
|
||||
this.props.navigation.goBack(props.scene.descriptor.key);
|
||||
});
|
||||
};
|
||||
return (
|
||||
<RenderedLeftComponent
|
||||
onPress={goBack}
|
||||
onPress={this._navigateBack}
|
||||
pressColorAndroid={options.headerPressColorAndroid}
|
||||
tintColor={options.headerTintColor}
|
||||
buttonImage={options.headerBackImage}
|
||||
@@ -165,7 +167,7 @@ class Header extends React.PureComponent {
|
||||
ButtonContainerComponent,
|
||||
LabelContainerComponent
|
||||
) => {
|
||||
const { options } = props.scene.descriptor;
|
||||
const { options } = this.props.getScreenDetails(props.scene);
|
||||
const backButtonTitle = this._getBackButtonTitleString(props.scene);
|
||||
const truncatedBackButtonTitle = this._getTruncatedBackButtonTitle(
|
||||
props.scene
|
||||
@@ -191,12 +193,13 @@ class Header extends React.PureComponent {
|
||||
};
|
||||
|
||||
_renderRightComponent = props => {
|
||||
const { headerRight } = props.scene.descriptor.options;
|
||||
const details = this.props.getScreenDetails(props.scene);
|
||||
const { headerRight } = details.options;
|
||||
return headerRight || null;
|
||||
};
|
||||
|
||||
_renderLeft(props) {
|
||||
const { options } = props.scene.descriptor;
|
||||
const { options } = this.props.getScreenDetails(props.scene);
|
||||
|
||||
const { transitionPreset } = this.props;
|
||||
|
||||
@@ -371,7 +374,7 @@ class Header extends React.PureComponent {
|
||||
});
|
||||
|
||||
const { isLandscape, transitionPreset } = this.props;
|
||||
const { options } = props.scene.descriptor;
|
||||
const { options } = this.props.getScreenDetails(props.scene);
|
||||
|
||||
const wrapperProps = {
|
||||
style: styles.header,
|
||||
@@ -436,7 +439,7 @@ class Header extends React.PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
const { options } = scene.descriptor;
|
||||
const { options } = this.props.getScreenDetails(scene);
|
||||
const { headerStyle = {} } = options;
|
||||
const headerStyleObj = StyleSheet.flatten(headerStyle);
|
||||
const appBarHeight = getAppBarHeight(isLandscape);
|
||||
|
||||
@@ -1,594 +0,0 @@
|
||||
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);
|
||||
@@ -1,176 +0,0 @@
|
||||
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,
|
||||
};
|
||||
@@ -1,40 +1,33 @@
|
||||
import React from 'react';
|
||||
import { Platform, StyleSheet, View } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import withLifecyclePolyfill from 'react-lifecycles-compat';
|
||||
|
||||
import SceneView from './SceneView';
|
||||
|
||||
const FAR_FAR_AWAY = 3000; // this should be big enough to move the whole view out of its container
|
||||
|
||||
export default class ResourceSavingSceneView extends React.PureComponent {
|
||||
class ResourceSavingSceneView extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super();
|
||||
|
||||
const key = props.childNavigation.state.key;
|
||||
const focusedIndex = props.navigation.state.index;
|
||||
const focusedKey = props.navigation.state.routes[focusedIndex].key;
|
||||
const isFocused = key === focusedKey;
|
||||
|
||||
this.state = {
|
||||
awake: props.lazy ? isFocused : true,
|
||||
visible: isFocused,
|
||||
awake: props.lazy ? props.isFocused : true,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this._actionListener = this.props.navigation.addListener(
|
||||
'action',
|
||||
this._onAction
|
||||
);
|
||||
}
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
if (nextProps.isFocused && !prevState.awake) {
|
||||
return { awake: true };
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._actionListener.remove();
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { awake, visible } = this.state;
|
||||
const { awake } = this.state;
|
||||
const {
|
||||
isFocused,
|
||||
childNavigation,
|
||||
navigation,
|
||||
removeClippedSubviews,
|
||||
@@ -49,12 +42,12 @@ export default class ResourceSavingSceneView extends React.PureComponent {
|
||||
removeClippedSubviews={
|
||||
Platform.OS === 'android'
|
||||
? removeClippedSubviews
|
||||
: !visible && removeClippedSubviews
|
||||
: !isFocused && removeClippedSubviews
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
this._mustAlwaysBeVisible() || visible
|
||||
this._mustAlwaysBeVisible() || isFocused
|
||||
? styles.innerAttached
|
||||
: styles.innerDetached
|
||||
}
|
||||
@@ -68,33 +61,6 @@ export default class ResourceSavingSceneView extends React.PureComponent {
|
||||
_mustAlwaysBeVisible = () => {
|
||||
return this.props.animationEnabled || this.props.swipeEnabled;
|
||||
};
|
||||
|
||||
_onAction = payload => {
|
||||
// We do not care about transition complete events, they won't actually change the state
|
||||
if (
|
||||
payload.action.type == 'Navigation/COMPLETE_TRANSITION' ||
|
||||
!payload.state
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { routes, index } = payload.state;
|
||||
const key = this.props.childNavigation.state.key;
|
||||
|
||||
if (routes[index].key === key) {
|
||||
if (!this.state.visible) {
|
||||
let nextState = { visible: true };
|
||||
if (!this.state.awake) {
|
||||
nextState.awake = true;
|
||||
}
|
||||
this.setState(nextState);
|
||||
}
|
||||
} else {
|
||||
if (this.state.visible) {
|
||||
this.setState({ visible: false });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
@@ -110,3 +76,5 @@ const styles = StyleSheet.create({
|
||||
top: FAR_FAR_AWAY,
|
||||
},
|
||||
});
|
||||
|
||||
export default withLifecyclePolyfill(ResourceSavingSceneView);
|
||||
|
||||
@@ -59,12 +59,7 @@ function areRoutesShallowEqual(one, two) {
|
||||
return shallowEqual(one, two);
|
||||
}
|
||||
|
||||
export default function ScenesReducer(
|
||||
scenes,
|
||||
nextState,
|
||||
prevState,
|
||||
descriptors
|
||||
) {
|
||||
export default function ScenesReducer(scenes, nextState, prevState) {
|
||||
if (prevState === nextState) {
|
||||
return scenes;
|
||||
}
|
||||
@@ -85,16 +80,12 @@ export default function ScenesReducer(
|
||||
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),
|
||||
@@ -118,16 +109,12 @@ export default function ScenesReducer(
|
||||
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,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
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;
|
||||
@@ -1,546 +0,0 @@
|
||||
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,268 +0,0 @@
|
||||
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,
|
||||
};
|
||||
@@ -1,225 +0,0 @@
|
||||
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;
|
||||
@@ -1,42 +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 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;
|
||||
}
|
||||
27
src/views/SwitchView/SwitchView.js
Normal file
27
src/views/SwitchView/SwitchView.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import SceneView from '../SceneView';
|
||||
import withCachedChildNavigation from '../../withCachedChildNavigation';
|
||||
|
||||
class SwitchContainer extends React.Component {
|
||||
render() {
|
||||
const { screenProps } = this.props;
|
||||
|
||||
const route = this.props.navigation.state.routes[
|
||||
this.props.navigation.state.index
|
||||
];
|
||||
const childNavigation = this.props.childNavigationProps[route.key];
|
||||
const ChildComponent = this.props.router.getComponentForRouteName(
|
||||
route.routeName
|
||||
);
|
||||
|
||||
return (
|
||||
<SceneView
|
||||
component={ChildComponent}
|
||||
navigation={childNavigation}
|
||||
screenProps={screenProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withCachedChildNavigation(SwitchContainer);
|
||||
@@ -100,6 +100,9 @@ class TabBarBottom extends React.PureComponent {
|
||||
if (showIcon === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const horizontal = this._shouldUseHorizontalTabs();
|
||||
|
||||
return (
|
||||
<TabBarIcon
|
||||
position={position}
|
||||
@@ -108,7 +111,11 @@ class TabBarBottom extends React.PureComponent {
|
||||
inactiveTintColor={inactiveTintColor}
|
||||
renderIcon={renderIcon}
|
||||
scene={scene}
|
||||
style={showLabel && this._shouldUseHorizontalTabs() ? {} : styles.icon}
|
||||
style={[
|
||||
styles.iconWithExplicitHeight,
|
||||
showLabel === false && !horizontal && styles.iconWithoutLabel,
|
||||
showLabel !== false && !horizontal && styles.iconWithLabel,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -286,6 +293,9 @@ class TabBarBottom extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_HEIGHT = 49;
|
||||
const COMPACT_HEIGHT = 29;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
tabBar: {
|
||||
backgroundColor: '#F7F7F7', // Default background color in iOS 10
|
||||
@@ -294,10 +304,10 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
},
|
||||
tabBarCompact: {
|
||||
height: 29,
|
||||
height: COMPACT_HEIGHT,
|
||||
},
|
||||
tabBarRegular: {
|
||||
height: 49,
|
||||
height: DEFAULT_HEIGHT,
|
||||
},
|
||||
tab: {
|
||||
flex: 1,
|
||||
@@ -311,8 +321,14 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
icon: {
|
||||
flexGrow: 1,
|
||||
iconWithoutLabel: {
|
||||
flex: 1,
|
||||
},
|
||||
iconWithLabel: {
|
||||
flex: 1,
|
||||
},
|
||||
iconWithExplicitHeight: {
|
||||
height: Platform.isPad ? DEFAULT_HEIGHT : COMPACT_HEIGHT,
|
||||
},
|
||||
label: {
|
||||
textAlign: 'center',
|
||||
|
||||
@@ -23,6 +23,7 @@ export default class TabBarIcon extends React.PureComponent {
|
||||
inputRange,
|
||||
outputRange: inputRange.map(i => (i === index ? 0 : 1)),
|
||||
});
|
||||
|
||||
// We render the icon twice at the same position on top of each other:
|
||||
// active and inactive one, so we can fade between them.
|
||||
return (
|
||||
@@ -53,11 +54,12 @@ 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:
|
||||
alignItems: 'center',
|
||||
alignSelf: 'center',
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
alignSelf: 'center',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
minWidth: 30,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ 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 = {
|
||||
@@ -21,33 +22,35 @@ class TabView extends React.PureComponent {
|
||||
};
|
||||
|
||||
_renderScene = ({ route }) => {
|
||||
const { screenProps, descriptors } = this.props;
|
||||
const {
|
||||
lazy,
|
||||
removeClippedSubviews,
|
||||
animationEnabled,
|
||||
swipeEnabled,
|
||||
} = this.props.navigationConfig;
|
||||
const descriptor = descriptors[route.key];
|
||||
const TabComponent = descriptor.getComponent();
|
||||
const { screenProps, navigation } = this.props;
|
||||
const focusedIndex = navigation.state.index;
|
||||
const focusedKey = navigation.state.routes[focusedIndex].key;
|
||||
const key = route.key;
|
||||
const childNavigation = this.props.childNavigationProps[route.key];
|
||||
const TabComponent = this.props.router.getComponentForRouteName(
|
||||
route.routeName
|
||||
);
|
||||
|
||||
return (
|
||||
<ResourceSavingSceneView
|
||||
lazy={lazy}
|
||||
removeClippedSubViews={removeClippedSubviews}
|
||||
animationEnabled={animationEnabled}
|
||||
swipeEnabled={swipeEnabled}
|
||||
lazy={this.props.lazy}
|
||||
isFocused={focusedKey === key}
|
||||
removeClippedSubViews={this.props.removeClippedSubviews}
|
||||
animationEnabled={this.props.animationEnabled}
|
||||
swipeEnabled={this.props.swipeEnabled}
|
||||
screenProps={screenProps}
|
||||
component={TabComponent}
|
||||
navigation={this.props.navigation}
|
||||
childNavigation={descriptor.navigation}
|
||||
childNavigation={childNavigation}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
_getLabel = ({ route, tintColor, focused }) => {
|
||||
const { screenProps, descriptors } = this.props;
|
||||
const descriptor = descriptors[route.key];
|
||||
const options = descriptor.options;
|
||||
const options = this.props.router.getScreenOptions(
|
||||
this.props.childNavigationProps[route.key],
|
||||
this.props.screenProps || {}
|
||||
);
|
||||
|
||||
if (options.tabBarLabel) {
|
||||
return typeof options.tabBarLabel === 'function'
|
||||
@@ -63,17 +66,19 @@ class TabView extends React.PureComponent {
|
||||
};
|
||||
|
||||
_getOnPress = (previousScene, { route }) => {
|
||||
const { descriptors } = this.props;
|
||||
const descriptor = descriptors[route.key];
|
||||
const options = descriptor.options;
|
||||
const options = this.props.router.getScreenOptions(
|
||||
this.props.childNavigationProps[route.key],
|
||||
this.props.screenProps || {}
|
||||
);
|
||||
|
||||
return options.tabBarOnPress;
|
||||
};
|
||||
|
||||
_getTestIDProps = ({ route }) => {
|
||||
const { descriptors } = this.props;
|
||||
const descriptor = descriptors[route.key];
|
||||
const options = descriptor.options;
|
||||
_getTestIDProps = ({ route, focused }) => {
|
||||
const options = this.props.router.getScreenOptions(
|
||||
this.props.childNavigationProps[route.key],
|
||||
this.props.screenProps || {}
|
||||
);
|
||||
|
||||
return typeof options.tabBarTestIDProps === 'function'
|
||||
? options.tabBarTestIDProps({ focused })
|
||||
@@ -81,10 +86,10 @@ class TabView extends React.PureComponent {
|
||||
};
|
||||
|
||||
_renderIcon = ({ focused, route, tintColor }) => {
|
||||
const { descriptors } = this.props;
|
||||
const descriptor = descriptors[route.key];
|
||||
const options = descriptor.options;
|
||||
|
||||
const options = this.props.router.getScreenOptions(
|
||||
this.props.childNavigationProps[route.key],
|
||||
this.props.screenProps || {}
|
||||
);
|
||||
if (options.tabBarIcon) {
|
||||
return typeof options.tabBarIcon === 'function'
|
||||
? options.tabBarIcon({ tintColor, focused })
|
||||
@@ -98,8 +103,7 @@ class TabView extends React.PureComponent {
|
||||
tabBarOptions,
|
||||
tabBarComponent: TabBarComponent,
|
||||
animationEnabled,
|
||||
tabBarPosition,
|
||||
} = this.props.navigationConfig;
|
||||
} = this.props;
|
||||
if (typeof TabBarComponent === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
@@ -108,7 +112,7 @@ class TabView extends React.PureComponent {
|
||||
<TabBarComponent
|
||||
{...props}
|
||||
{...tabBarOptions}
|
||||
tabBarPosition={tabBarPosition}
|
||||
tabBarPosition={this.props.tabBarPosition}
|
||||
screenProps={this.props.screenProps}
|
||||
navigation={this.props.navigation}
|
||||
getLabel={this._getLabel}
|
||||
@@ -124,29 +128,31 @@ class TabView extends React.PureComponent {
|
||||
|
||||
render() {
|
||||
const {
|
||||
router,
|
||||
tabBarComponent,
|
||||
tabBarPosition,
|
||||
animationEnabled,
|
||||
configureTransition,
|
||||
initialLayout,
|
||||
} = this.props.navigationConfig;
|
||||
screenProps,
|
||||
} = this.props;
|
||||
|
||||
let renderHeader;
|
||||
let renderFooter;
|
||||
let renderPager;
|
||||
|
||||
const { state } = this.props.navigation;
|
||||
const route = state.routes[state.index];
|
||||
const { descriptors } = this.props;
|
||||
const descriptor = descriptors[route.key];
|
||||
const options = descriptor.options;
|
||||
const options = router.getScreenOptions(
|
||||
this.props.childNavigationProps[state.routes[state.index].key],
|
||||
screenProps || {}
|
||||
);
|
||||
|
||||
const tabBarVisible =
|
||||
options.tabBarVisible == null ? true : options.tabBarVisible;
|
||||
|
||||
let swipeEnabled =
|
||||
options.swipeEnabled == null
|
||||
? this.props.navigationConfig.swipeEnabled
|
||||
? this.props.swipeEnabled
|
||||
: options.swipeEnabled;
|
||||
|
||||
if (typeof swipeEnabled === 'function') {
|
||||
@@ -179,6 +185,7 @@ class TabView extends React.PureComponent {
|
||||
renderScene: this._renderScene,
|
||||
onIndexChange: this._handlePageChanged,
|
||||
navigationState: this.props.navigation.state,
|
||||
screenProps: this.props.screenProps,
|
||||
style: styles.container,
|
||||
};
|
||||
|
||||
@@ -186,7 +193,7 @@ class TabView extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
export default TabView;
|
||||
export default withCachedChildNavigation(TabView);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
|
||||
@@ -29,12 +29,7 @@ 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,
|
||||
null,
|
||||
this.props.descriptors
|
||||
),
|
||||
scenes: NavigationScenesReducer([], this.props.navigation.state),
|
||||
};
|
||||
|
||||
this._prevTransitionProps = null;
|
||||
@@ -61,8 +56,7 @@ class Transitioner extends React.Component {
|
||||
const nextScenes = NavigationScenesReducer(
|
||||
this.state.scenes,
|
||||
nextProps.navigation.state,
|
||||
this.props.navigation.state,
|
||||
nextProps.descriptors
|
||||
this.props.navigation.state
|
||||
);
|
||||
|
||||
if (nextScenes === this.state.scenes) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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';
|
||||
@@ -11,30 +12,21 @@ const dummyEventSubscriber = (name, handler) => ({
|
||||
|
||||
describe('TabBarBottom', () => {
|
||||
it('renders successfully', () => {
|
||||
const route = { key: 's1', routeName: 's1' };
|
||||
const navigation = {
|
||||
state: {
|
||||
index: 0,
|
||||
routes: [route],
|
||||
routes: [{ key: 's1', routeName: 's1' }],
|
||||
},
|
||||
addListener: dummyEventSubscriber,
|
||||
};
|
||||
const router = TabRouter({ s1: { screen: View } });
|
||||
|
||||
const rendered = renderer
|
||||
.create(
|
||||
<TabView
|
||||
tabBarComponent={TabBarBottom}
|
||||
navigation={navigation}
|
||||
navigationConfig={{}}
|
||||
descriptors={{
|
||||
s1: {
|
||||
state: route,
|
||||
key: route.key,
|
||||
options: {},
|
||||
navigation: { state: route },
|
||||
getComponent: () => View,
|
||||
},
|
||||
}}
|
||||
router={router}
|
||||
/>
|
||||
)
|
||||
.toJSON();
|
||||
|
||||
@@ -20,6 +20,131 @@ 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={
|
||||
Array [
|
||||
Object {
|
||||
"height": 29,
|
||||
},
|
||||
false,
|
||||
Object {
|
||||
"flexGrow": 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"alignSelf": "center",
|
||||
"height": "100%",
|
||||
"justifyContent": "center",
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"alignSelf": "center",
|
||||
"height": "100%",
|
||||
"justifyContent": "center",
|
||||
"opacity": 0,
|
||||
"position": "absolute",
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
accessible={true}
|
||||
allowFontScaling={true}
|
||||
collapsable={undefined}
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "transparent",
|
||||
"color": "rgba(52, 120, 246, 1)",
|
||||
"fontSize": 10,
|
||||
"marginBottom": 1.5,
|
||||
"textAlign": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
s1
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<RCTScrollView
|
||||
DEPRECATED_sendUpdatedChildFrames={false}
|
||||
alwaysBounceHorizontal={false}
|
||||
@@ -123,6 +248,17 @@ exports[`TabBarBottom renders successfully 1`] = `
|
||||
<View
|
||||
navigation={
|
||||
Object {
|
||||
"addListener": [Function],
|
||||
"dispatch": undefined,
|
||||
"getParam": [Function],
|
||||
"goBack": [Function],
|
||||
"isFocused": [Function],
|
||||
"navigate": [Function],
|
||||
"pop": [Function],
|
||||
"popToTop": [Function],
|
||||
"push": [Function],
|
||||
"replace": [Function],
|
||||
"setParams": [Function],
|
||||
"state": Object {
|
||||
"key": "s1",
|
||||
"routeName": "s1",
|
||||
|
||||
76
src/withCachedChildNavigation.js
Normal file
76
src/withCachedChildNavigation.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import addNavigationHelpers from './addNavigationHelpers';
|
||||
import getChildEventSubscriber from './getChildEventSubscriber';
|
||||
|
||||
/**
|
||||
* HOC which caches the child navigation items.
|
||||
*/
|
||||
export default function withCachedChildNavigation(Comp) {
|
||||
const displayName = Comp.displayName || Comp.name;
|
||||
return class extends React.PureComponent {
|
||||
static displayName = `withCachedChildNavigation(${displayName})`;
|
||||
|
||||
_childEventSubscribers = {};
|
||||
|
||||
componentWillMount() {
|
||||
this._updateNavigationProps(this.props.navigation);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this._updateNavigationProps(nextProps.navigation);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const activeKeys = this.props.navigation.state.routes.map(
|
||||
route => route.key
|
||||
);
|
||||
Object.keys(this._childEventSubscribers).forEach(key => {
|
||||
if (!activeKeys.includes(key)) {
|
||||
delete this._childEventSubscribers[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_isRouteFocused = route => {
|
||||
const { state } = this.props.navigation;
|
||||
const focusedRoute = state.routes[state.index];
|
||||
return route === focusedRoute;
|
||||
};
|
||||
|
||||
_updateNavigationProps = navigation => {
|
||||
// Update props for each child route
|
||||
if (!this._childNavigationProps) {
|
||||
this._childNavigationProps = {};
|
||||
}
|
||||
navigation.state.routes.forEach(route => {
|
||||
const childNavigation = this._childNavigationProps[route.key];
|
||||
if (childNavigation && childNavigation.state === route) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._childEventSubscribers[route.key]) {
|
||||
this._childEventSubscribers[route.key] = getChildEventSubscriber(
|
||||
navigation.addListener,
|
||||
route.key
|
||||
);
|
||||
}
|
||||
|
||||
this._childNavigationProps[route.key] = addNavigationHelpers({
|
||||
dispatch: navigation.dispatch,
|
||||
state: route,
|
||||
isFocused: () => this._isRouteFocused(route),
|
||||
addListener: this._childEventSubscribers[route.key],
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Comp
|
||||
{...this.props}
|
||||
childNavigationProps={this._childNavigationProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user