Compare commits

..

7 Commits

Author SHA1 Message Date
Brent Vatne
27eb73cc14 Release 2.12.1 2018-08-23 16:29:12 -07:00
Brent Vatne
f01b4896e6 Release 2.12.0 2018-08-22 17:17:54 -07:00
Brent Vatne
556c31626e Update react-navigation-stack, export HeaderStyleInterpolator, and add example of using it to playground 2018-08-22 17:16:39 -07:00
Brent Vatne
b6bca3ed2e Fix drawer config in Drawer + Tabs example 2018-08-22 16:04:11 -07:00
Nicolas Charpentier
0c56b21b46 Remove duplicate private key (#4819)
A package can't be more private than that, so let's remove duplicates.
2018-08-21 16:13:22 -07:00
Kevin Coleman
912c7ca076 Bump react-native-safe-area-view dep to 0.9.0 (#4810)
https://github.com/react-navigation/react-navigation/issues/3992
2018-08-07 10:23:51 -07:00
Brent Vatne
73c76f1e4b Extract stack to react-navigation-stack (#4809)
* Extract stack to react-navigation-stack

* Update CHANGELOG

* Fix tests
2018-08-03 18:47:46 -07:00
30 changed files with 88 additions and 4354 deletions

View File

@@ -6,6 +6,21 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [2.12.1] - [2018-08-23](https://github.com/react-navigation/react-navigation/releases/tag/2.12.1)
### Fixed
- Fix crash on react-native@>=0.56 described in https://github.com/react-navigation/react-navigation/issues/4886
## [2.12.0] - [2018-08-22](https://github.com/react-navigation/react-navigation/releases/tag/2.12.0)
### Changed
- Move stack specific view code to react-navigation-stack
- Add accessibility props for inactive screens in stack (https://github.com/react-navigation/react-navigation-stack/commit/4e04428e26df9076413b57b3346a7ce357de1a77)
- Updated header title to match iOS 11/12 style correctly (https://github.com/react-navigation/react-navigation-stack/pull/1)
- Add support for animating the header background on screen transitions and add interpolator to animate it along with the rest of the screen, but this is still opt-in behavior (https://github.com/react-navigation/react-navigation-stack/pull/3)
- Updated react-native-safe-area-view to 0.2.0
## [2.11.2] - [2018-08-03](https://github.com/react-navigation/react-navigation/releases/tag/2.11.2)
### Changed
- Revert rename of pathUtils
@@ -138,7 +153,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
### Changed
- Improved examples
[Unreleased]: https://github.com/react-navigation/react-navigation/compare/2.11.2...HEAD
[Unreleased]: https://github.com/react-navigation/react-navigation/compare/2.12.1...HEAD
[2.12.1]: https://github.com/react-navigation/react-navigation/compare/2.12.0...2.12.1
[2.12.0]: https://github.com/react-navigation/react-navigation/compare/2.11.2...2.12.0
[2.11.2]: https://github.com/react-navigation/react-navigation/compare/2.11.1...2.11.2
[2.11.1]: https://github.com/react-navigation/react-navigation/compare/2.11.0...2.11.1
[2.11.0]: https://github.com/react-navigation/react-navigation/compare/2.10.0...2.11.0

View File

@@ -19,7 +19,11 @@ import {
StyleSheet,
View,
} from 'react-native';
import { Header, createStackNavigator } from 'react-navigation';
import {
Header,
HeaderStyleInterpolator,
createStackNavigator,
} from 'react-navigation';
import invariant from 'invariant';
import SampleText from './SampleText';
@@ -230,6 +234,14 @@ const StackWithTranslucentHeader = createStackNavigator(
},
{
headerTransitionPreset: 'uikit',
// You can leave this out if you don't want the card shadow to
// be visible through the header
transitionConfig: () => ({
headerBackgroundInterpolator:
HeaderStyleInterpolator.forBackgroundWithTranslation,
}),
navigationOptions: {
headerTransparent: true,
headerStyle: {

View File

@@ -13,31 +13,19 @@ const TabsInDrawer = createDrawerNavigator({
SimpleTabs: {
screen: SimpleTabs,
navigationOptions: {
drawer: () => ({
label: 'Simple Tabs',
icon: ({ tintColor }) => (
<MaterialIcons
name="filter-1"
size={24}
style={{ color: tintColor }}
/>
),
}),
drawerLabel: 'Simple tabs',
drawerIcon: ({ tintColor }) => (
<MaterialIcons name="filter-1" size={24} style={{ color: tintColor }} />
),
},
},
StacksOverTabs: {
screen: StacksOverTabs,
navigationOptions: {
drawer: () => ({
label: 'Stacks Over Tabs',
icon: ({ tintColor }) => (
<MaterialIcons
name="filter-2"
size={24}
style={{ color: tintColor }}
/>
),
}),
drawerLabel: 'Stacks Over Tabs',
drawerIcon: ({ tintColor }) => (
<MaterialIcons name="filter-2" size={24} style={{ color: tintColor }} />
),
},
},
});

View File

@@ -3,7 +3,6 @@
"version": "0.1.0",
"private": true,
"main": "node_modules/expo/AppEntry.js",
"private": true,
"scripts": {
"start": "expo start",
"android": "expo start --android",

View File

@@ -4480,9 +4480,9 @@ react-native-safe-area-view@^0.7.0:
dependencies:
hoist-non-react-statics "^2.3.1"
react-native-safe-area-view@^0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/react-native-safe-area-view/-/react-native-safe-area-view-0.8.0.tgz#22d78cb8e8658d04a10cd53c1546e0bc86cb7aea"
react-native-safe-area-view@^0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/react-native-safe-area-view/-/react-native-safe-area-view-0.9.0.tgz#10ece2ecac70e7005a5b0b3f06c8833060e6d21f"
dependencies:
hoist-non-react-statics "^2.3.1"
@@ -4590,9 +4590,9 @@ react-navigation-deprecated-tab-navigator@1.3.0:
dependencies:
react-native-tab-view "^0.0.77"
react-navigation-drawer@0.4.3:
version "0.4.3"
resolved "https://registry.yarnpkg.com/react-navigation-drawer/-/react-navigation-drawer-0.4.3.tgz#c04c94e2429b7e724801af05bd0a93a79cb27f71"
react-navigation-drawer@0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/react-navigation-drawer/-/react-navigation-drawer-0.5.0.tgz#d91b6a6ec65c34ba78c00f814b1e6508922cc9ec"
dependencies:
react-native-drawer-layout-polyfill "^1.3.2"
@@ -4611,9 +4611,13 @@ react-navigation-material-bottom-tabs@^0.3.0:
react-native-paper "2.0.0-alpha.4"
react-navigation-tabs "^0.4.0"
react-navigation-tabs@0.5.1, react-navigation-tabs@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/react-navigation-tabs/-/react-navigation-tabs-0.5.1.tgz#ed33bce3a3e21b92646700de25bd94b8fc570371"
react-navigation-stack@0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/react-navigation-stack/-/react-navigation-stack-0.2.3.tgz#9d1e2524aa1d178302c938948b8ece49d713f12b"
react-navigation-tabs@0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/react-navigation-tabs/-/react-navigation-tabs-0.6.0.tgz#2f526194f4360e56c2702e736887449acc2080dc"
dependencies:
hoist-non-react-statics "^2.5.0"
prop-types "^15.6.1"
@@ -4631,6 +4635,16 @@ react-navigation-tabs@^0.4.0:
react-native-safe-area-view "^0.7.0"
react-native-tab-view "^1.0.0"
react-navigation-tabs@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/react-navigation-tabs/-/react-navigation-tabs-0.5.1.tgz#ed33bce3a3e21b92646700de25bd94b8fc570371"
dependencies:
hoist-non-react-statics "^2.5.0"
prop-types "^15.6.1"
react-lifecycles-compat "^3.0.4"
react-native-safe-area-view "^0.7.0"
react-native-tab-view "^1.0.0"
"react-navigation@link:../..":
version "0.0.0"
uid ""

View File

@@ -1,6 +1,6 @@
{
"name": "react-navigation",
"version": "2.11.2",
"version": "2.12.1",
"description": "Routing and navigation for your React Native apps",
"main": "src/react-navigation.js",
"repository": {
@@ -36,9 +36,10 @@
"path-to-regexp": "^1.7.0",
"query-string": "^6.1.0",
"react-lifecycles-compat": "^3",
"react-native-safe-area-view": "^0.8.0",
"react-native-safe-area-view": "^0.9.0",
"react-navigation-deprecated-tab-navigator": "1.3.0",
"react-navigation-drawer": "0.5.0",
"react-navigation-stack": "0.2.3",
"react-navigation-tabs": "0.6.0"
},
"devDependencies": {
@@ -90,7 +91,7 @@
"<rootDir>/examples/"
],
"transformIgnorePatterns": [
"node_modules/(?!(jest-)?react-native|react-clone-referenced-element|react-navigation-deprecated-tab-navigator)"
"node_modules/(?!(jest-)?react-native|react-clone-referenced-element|react-navigation-deprecated-tab-navigator|react-navigation-stack)"
]
},
"lint-staged": {

View File

@@ -4,7 +4,9 @@ import { StyleSheet, View } from 'react-native';
import renderer from 'react-test-renderer';
import NavigationActions from '../NavigationActions';
import createStackNavigator from '../navigators/createStackNavigator';
// TODO: we should create a dummy navigator here
import { createStackNavigator } from 'react-navigation-stack';
import createNavigationContainer, {
_TESTING_ONLY_reset_container_count,
} from '../createNavigationContainer';

View File

@@ -1,40 +0,0 @@
import React from 'react';
import renderer from 'react-test-renderer';
import StackNavigator from '../createContainedStackNavigator';
const SubNavigator = StackNavigator({
Home: {
screen: () => null,
},
});
const NavNestedDirect = StackNavigator({
Sub: {
screen: SubNavigator,
},
});
const NavNestedIndirect = StackNavigator({
Sub: {
// eslint-disable-next-line react/display-name
screen: props => <SubNavigator {...props} />,
},
});
/* Prevent React error boundaries from swallowing the errors */
NavNestedIndirect.prototype.componentDidCatch = null;
SubNavigator.prototype.componentDidCatch = null;
describe('Nested navigators', () => {
it('renders succesfully as direct child', () => {
const rendered = renderer.create(<NavNestedDirect />).toJSON();
expect(rendered).toMatchSnapshot();
});
it('throw when trying to pass navigation prop', () => {
const tryRender = () => {
renderer.create(<NavNestedIndirect />);
};
expect(tryRender).toThrowErrorMatchingSnapshot();
});
});

View File

@@ -1,93 +0,0 @@
import React, { Component } from 'react';
import { StyleSheet, View } from 'react-native';
import renderer from 'react-test-renderer';
import StackNavigator from '../createContainedStackNavigator';
import withNavigation from '../../views/withNavigation';
import { _TESTING_ONLY_reset_container_count } from '../../createNavigationContainer';
const styles = StyleSheet.create({
header: {
opacity: 0.5,
},
});
class HomeScreen extends Component {
static navigationOptions = ({ navigation }) => ({
title: `Welcome ${
navigation.state.params ? navigation.state.params.user : 'anonymous'
}`,
gesturesEnabled: true,
headerStyle: [{ backgroundColor: 'red' }, styles.header],
});
render() {
return null;
}
}
const routeConfig = {
Home: {
screen: HomeScreen,
},
};
describe('StackNavigator', () => {
beforeEach(() => {
_TESTING_ONLY_reset_container_count();
});
it('renders successfully', () => {
const MyStackNavigator = StackNavigator(routeConfig);
const rendered = renderer.create(<MyStackNavigator />).toJSON();
expect(rendered).toMatchSnapshot();
});
it('applies correct values when headerRight is present', () => {
const MyStackNavigator = StackNavigator({
Home: {
screen: HomeScreen,
navigationOptions: {
headerRight: <View />,
},
},
});
const rendered = renderer.create(<MyStackNavigator />).toJSON();
expect(rendered).toMatchSnapshot();
});
it('passes navigation to headerRight when wrapped in withNavigation', () => {
const spy = jest.fn();
class TestComponent extends React.Component {
render() {
return <View>{this.props.onPress(this.props.navigation)}</View>;
}
}
const TestComponentWithNavigation = withNavigation(TestComponent);
class A extends React.Component {
static navigationOptions = {
headerRight: <TestComponentWithNavigation onPress={spy} />,
};
render() {
return <View />;
}
}
const Nav = StackNavigator({ A: { screen: A } });
renderer.create(<Nav />);
expect(spy).toBeCalledWith(
expect.objectContaining({
navigate: expect.any(Function),
addListener: expect.any(Function),
})
);
});
});

View File

@@ -1,367 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Nested navigators renders succesfully as direct child 1`] = `
<View
onLayout={[Function]}
style={
Object {
"flex": 1,
}
}
>
<View
onMoveShouldSetResponder={[Function]}
onMoveShouldSetResponderCapture={[Function]}
onResponderEnd={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderReject={[Function]}
onResponderRelease={[Function]}
onResponderStart={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
style={
Array [
Object {
"flex": 1,
"flexDirection": "column-reverse",
},
Object {
"backgroundColor": "#000",
},
]
}
>
<View
style={
Object {
"flex": 1,
}
}
>
<View
collapsable={undefined}
pointerEvents="auto"
style={
Object {
"backgroundColor": "#E9E9EF",
"bottom": 0,
"left": 0,
"marginTop": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"shadowColor": "black",
"shadowOffset": Object {
"height": 0,
"width": 0,
},
"shadowOpacity": 0.2,
"shadowRadius": 5,
"top": 0,
"transform": Array [
Object {
"translateX": 0,
},
Object {
"translateY": 0,
},
],
}
}
>
<View
onLayout={[Function]}
style={
Object {
"flex": 1,
}
}
>
<View
onMoveShouldSetResponder={[Function]}
onMoveShouldSetResponderCapture={[Function]}
onResponderEnd={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderReject={[Function]}
onResponderRelease={[Function]}
onResponderStart={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
style={
Array [
Object {
"flex": 1,
"flexDirection": "column-reverse",
},
Object {
"backgroundColor": "#000",
},
]
}
>
<View
style={
Object {
"flex": 1,
}
}
>
<View
collapsable={undefined}
pointerEvents="auto"
style={
Object {
"backgroundColor": "#E9E9EF",
"bottom": 0,
"left": 0,
"marginTop": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"shadowColor": "black",
"shadowOffset": Object {
"height": 0,
"width": 0,
},
"shadowOpacity": 0.2,
"shadowRadius": 5,
"top": 0,
"transform": Array [
Object {
"translateX": 0,
},
Object {
"translateY": 0,
},
],
}
}
/>
</View>
<View
onLayout={[Function]}
pointerEvents="box-none"
>
<View
collapsable={undefined}
style={
Object {
"backgroundColor": "#F7F7F7",
"transform": Array [
Object {
"translateX": 0,
},
],
}
}
>
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"backgroundColor": "#F7F7F7",
"borderBottomColor": "#A7A7AA",
"borderBottomWidth": 0.5,
"height": 64,
"paddingBottom": 0,
"paddingLeft": 0,
"paddingRight": 0,
"paddingTop": 20,
}
}
>
<View
style={
Object {
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
<View
style={
Object {
"flex": 1,
}
}
>
<View
style={
Object {
"bottom": 0,
"flexDirection": "row",
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
>
<View
collapsable={undefined}
pointerEvents="box-none"
style={
Object {
"alignItems": "center",
"backgroundColor": "transparent",
"bottom": 0,
"flexDirection": "row",
"justifyContent": "center",
"left": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"top": 0,
}
}
>
<Text
accessibilityTraits="header"
accessible={true}
allowFontScaling={true}
collapsable={undefined}
ellipsizeMode="tail"
numberOfLines={1}
onLayout={[Function]}
style={
Object {
"color": "rgba(0, 0, 0, .9)",
"fontSize": 17,
"fontWeight": "700",
"marginHorizontal": 16,
"textAlign": "center",
}
}
/>
</View>
</View>
</View>
</View>
</View>
</View>
</View>
</View>
</View>
</View>
<View
onLayout={[Function]}
pointerEvents="box-none"
>
<View
collapsable={undefined}
style={
Object {
"backgroundColor": "#F7F7F7",
"transform": Array [
Object {
"translateX": 0,
},
],
}
}
>
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"backgroundColor": "#F7F7F7",
"borderBottomColor": "#A7A7AA",
"borderBottomWidth": 0.5,
"height": 64,
"paddingBottom": 0,
"paddingLeft": 0,
"paddingRight": 0,
"paddingTop": 20,
}
}
>
<View
style={
Object {
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
<View
style={
Object {
"flex": 1,
}
}
>
<View
style={
Object {
"bottom": 0,
"flexDirection": "row",
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
>
<View
collapsable={undefined}
pointerEvents="box-none"
style={
Object {
"alignItems": "center",
"backgroundColor": "transparent",
"bottom": 0,
"flexDirection": "row",
"justifyContent": "center",
"left": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"top": 0,
}
}
>
<Text
accessibilityTraits="header"
accessible={true}
allowFontScaling={true}
collapsable={undefined}
ellipsizeMode="tail"
numberOfLines={1}
onLayout={[Function]}
style={
Object {
"color": "rgba(0, 0, 0, .9)",
"fontSize": 17,
"fontWeight": "700",
"marginHorizontal": 16,
"textAlign": "center",
}
}
/>
</View>
</View>
</View>
</View>
</View>
</View>
</View>
</View>
`;
exports[`Nested navigators throw when trying to pass navigation prop 1`] = `"No \\"routes\\" found in navigation state. Did you try to pass the navigation prop of a React component to a Navigator child? See https://reactnavigation.org/docs/en/custom-navigators.html#navigator-navigation-prop"`;

View File

@@ -1,391 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`StackNavigator applies correct values when headerRight is present 1`] = `
<View
onLayout={[Function]}
style={
Object {
"flex": 1,
}
}
>
<View
onMoveShouldSetResponder={[Function]}
onMoveShouldSetResponderCapture={[Function]}
onResponderEnd={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderReject={[Function]}
onResponderRelease={[Function]}
onResponderStart={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
style={
Array [
Object {
"flex": 1,
"flexDirection": "column-reverse",
},
Object {
"backgroundColor": "#000",
},
]
}
>
<View
style={
Object {
"flex": 1,
}
}
>
<View
collapsable={undefined}
pointerEvents="auto"
style={
Object {
"backgroundColor": "#E9E9EF",
"bottom": 0,
"left": 0,
"marginTop": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"shadowColor": "black",
"shadowOffset": Object {
"height": 0,
"width": 0,
},
"shadowOpacity": 0.2,
"shadowRadius": 5,
"top": 0,
"transform": Array [
Object {
"translateX": 0,
},
Object {
"translateY": 0,
},
],
}
}
/>
</View>
<View
onLayout={[Function]}
pointerEvents="box-none"
>
<View
collapsable={undefined}
style={
Object {
"backgroundColor": "red",
"transform": Array [
Object {
"translateX": 0,
},
],
}
}
>
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"backgroundColor": "red",
"borderBottomColor": "#A7A7AA",
"borderBottomWidth": 0.5,
"height": 64,
"opacity": 0.5,
"paddingBottom": 0,
"paddingLeft": 0,
"paddingRight": 0,
"paddingTop": 20,
}
}
>
<View
style={
Object {
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
<View
style={
Object {
"flex": 1,
}
}
>
<View
style={
Object {
"bottom": 0,
"flexDirection": "row",
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
>
<View
collapsable={undefined}
pointerEvents="box-none"
style={
Object {
"alignItems": "center",
"backgroundColor": "transparent",
"bottom": 0,
"flexDirection": "row",
"justifyContent": "center",
"left": 70,
"opacity": 1,
"position": "absolute",
"right": 70,
"top": 0,
}
}
>
<Text
accessibilityTraits="header"
accessible={true}
allowFontScaling={true}
collapsable={undefined}
ellipsizeMode="tail"
numberOfLines={1}
onLayout={[Function]}
style={
Object {
"color": "rgba(0, 0, 0, .9)",
"fontSize": 17,
"fontWeight": "700",
"marginHorizontal": 16,
"textAlign": "center",
}
}
>
Welcome anonymous
</Text>
</View>
<View
collapsable={undefined}
pointerEvents="box-none"
style={
Object {
"alignItems": "center",
"backgroundColor": "transparent",
"bottom": 0,
"flexDirection": "row",
"opacity": 1,
"position": "absolute",
"right": 0,
"top": 0,
}
}
>
<View />
</View>
</View>
</View>
</View>
</View>
</View>
</View>
</View>
`;
exports[`StackNavigator renders successfully 1`] = `
<View
onLayout={[Function]}
style={
Object {
"flex": 1,
}
}
>
<View
onMoveShouldSetResponder={[Function]}
onMoveShouldSetResponderCapture={[Function]}
onResponderEnd={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderReject={[Function]}
onResponderRelease={[Function]}
onResponderStart={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
style={
Array [
Object {
"flex": 1,
"flexDirection": "column-reverse",
},
Object {
"backgroundColor": "#000",
},
]
}
>
<View
style={
Object {
"flex": 1,
}
}
>
<View
collapsable={undefined}
pointerEvents="auto"
style={
Object {
"backgroundColor": "#E9E9EF",
"bottom": 0,
"left": 0,
"marginTop": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"shadowColor": "black",
"shadowOffset": Object {
"height": 0,
"width": 0,
},
"shadowOpacity": 0.2,
"shadowRadius": 5,
"top": 0,
"transform": Array [
Object {
"translateX": 0,
},
Object {
"translateY": 0,
},
],
}
}
/>
</View>
<View
onLayout={[Function]}
pointerEvents="box-none"
>
<View
collapsable={undefined}
style={
Object {
"backgroundColor": "red",
"transform": Array [
Object {
"translateX": 0,
},
],
}
}
>
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"backgroundColor": "red",
"borderBottomColor": "#A7A7AA",
"borderBottomWidth": 0.5,
"height": 64,
"opacity": 0.5,
"paddingBottom": 0,
"paddingLeft": 0,
"paddingRight": 0,
"paddingTop": 20,
}
}
>
<View
style={
Object {
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
<View
style={
Object {
"flex": 1,
}
}
>
<View
style={
Object {
"bottom": 0,
"flexDirection": "row",
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
>
<View
collapsable={undefined}
pointerEvents="box-none"
style={
Object {
"alignItems": "center",
"backgroundColor": "transparent",
"bottom": 0,
"flexDirection": "row",
"justifyContent": "center",
"left": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"top": 0,
}
}
>
<Text
accessibilityTraits="header"
accessible={true}
allowFontScaling={true}
collapsable={undefined}
ellipsizeMode="tail"
numberOfLines={1}
onLayout={[Function]}
style={
Object {
"color": "rgba(0, 0, 0, .9)",
"fontSize": 17,
"fontWeight": "700",
"marginHorizontal": 16,
"textAlign": "center",
}
}
>
Welcome anonymous
</Text>
</View>
</View>
</View>
</View>
</View>
</View>
</View>
</View>
`;

View File

@@ -1,9 +0,0 @@
import createNavigationContainer from '../createNavigationContainer';
import createStackNavigator from './createStackNavigator';
const StackNavigator = (routeConfigs, config = {}) => {
const navigator = createStackNavigator(routeConfigs, config);
return createNavigationContainer(navigator);
};
export default StackNavigator;

View File

@@ -1,37 +0,0 @@
import createKeyboardAwareNavigator from './createKeyboardAwareNavigator';
import createNavigator from './createNavigator';
import StackView from '../views/StackView/StackView';
import StackRouter from '../routers/StackRouter';
function createStackNavigator(routeConfigMap, stackConfig = {}) {
const {
initialRouteKey,
initialRouteName,
initialRouteParams,
paths,
navigationOptions,
disableKeyboardHandling,
getCustomActionCreators,
} = stackConfig;
const stackRouterConfig = {
initialRouteKey,
initialRouteName,
initialRouteParams,
paths,
navigationOptions,
getCustomActionCreators,
};
const router = StackRouter(routeConfigMap, stackRouterConfig);
// Create a navigator with StackView as the view
let Navigator = createNavigator(StackView, router, stackConfig);
if (!disableKeyboardHandling) {
Navigator = createKeyboardAwareNavigator(Navigator, stackConfig);
}
return Navigator;
}
export default createStackNavigator;

View File

@@ -26,13 +26,13 @@ module.exports = {
return require('./views/NavigationContext').default.NavigationConsumer;
},
get createStackNavigator() {
return require('./navigators/createContainedStackNavigator').default;
return require('react-navigation-stack').createStackNavigator;
},
get StackNavigator() {
console.warn(
'The StackNavigator function name is deprecated, please use createStackNavigator instead'
);
return require('./navigators/createContainedStackNavigator').default;
return require('react-navigation-stack').createStackNavigator;
},
get createSwitchNavigator() {
return require('./navigators/createContainedSwitchNavigator').default;
@@ -112,16 +112,16 @@ module.exports = {
// Views
get Transitioner() {
return require('./views/Transitioner').default;
return require('react-navigation-stack').Transitioner;
},
get StackView() {
return require('./views/StackView/StackView').default;
return require('react-navigation-stack').StackView;
},
get StackViewCard() {
return require('./views/StackView/StackViewCard').default;
return require('react-navigation-stack').StackViewCard;
},
get StackViewTransitionConfigs() {
return require('./views/StackView/StackViewTransitionConfigs').default;
return require('react-navigation-stack').StackViewTransitionConfigs;
},
get SafeAreaView() {
return require('react-native-safe-area-view').default;
@@ -135,13 +135,16 @@ module.exports = {
// Header
get Header() {
return require('./views/Header/Header').default;
return require('react-navigation-stack').Header;
},
get HeaderTitle() {
return require('./views/Header/HeaderTitle').default;
return require('react-navigation-stack').HeaderTitle;
},
get HeaderBackButton() {
return require('./views/Header/HeaderBackButton').default;
return require('react-navigation-stack').HeaderBackButton;
},
get HeaderStyleInterpolator() {
return require('react-navigation-stack').HeaderStyleInterpolator;
},
// DrawerView

View File

@@ -1,705 +0,0 @@
import React from 'react';
import {
Animated,
Dimensions,
Image,
Platform,
StyleSheet,
View,
I18nManager,
ViewPropTypes,
MaskedViewIOS,
} from 'react-native';
import SafeAreaView from 'react-native-safe-area-view';
import HeaderTitle from './HeaderTitle';
import HeaderBackButton from './HeaderBackButton';
import ModularHeaderBackButton from './ModularHeaderBackButton';
import HeaderStyleInterpolator from './HeaderStyleInterpolator';
import withOrientation from '../withOrientation';
const APPBAR_HEIGHT = Platform.OS === 'ios' ? 44 : 56;
const STATUSBAR_HEIGHT = Platform.OS === 'ios' ? 20 : 0;
// These can be adjusted by using headerTitleContainerStyle on navigationOptions
const TITLE_OFFSET_CENTER_ALIGN = Platform.OS === 'ios' ? 70 : 56;
const TITLE_OFFSET_LEFT_ALIGN = Platform.OS === 'ios' ? 20 : 56;
const getTitleOffsets = (
layoutPreset,
forceBackTitle,
hasLeftComponent,
hasRightComponent
) => {
if (layoutPreset === 'left') {
// Maybe at some point we should do something different if the back title is
// explicitly enabled, for now people can control it manually
let style = {
left: TITLE_OFFSET_LEFT_ALIGN,
right: TITLE_OFFSET_LEFT_ALIGN,
};
if (!hasLeftComponent) {
style.left = 0;
}
if (!hasRightComponent) {
style.right = 0;
}
return style;
} else if (layoutPreset === 'center') {
let style = {
left: TITLE_OFFSET_CENTER_ALIGN,
right: TITLE_OFFSET_CENTER_ALIGN,
};
if (!hasLeftComponent && !hasRightComponent) {
style.left = 0;
style.right = 0;
}
return style;
}
};
const getAppBarHeight = isLandscape => {
return Platform.OS === 'ios'
? isLandscape && !Platform.isPad
? 32
: 44
: 56;
};
class Header extends React.PureComponent {
static defaultProps = {
layoutInterpolator: HeaderStyleInterpolator.forLayout,
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(scene) {
const options = scene.descriptor.options;
if (typeof options.headerTitle === 'string') {
return options.headerTitle;
}
if (options.title && typeof options.title !== 'string' && __DEV__) {
throw new Error(
`Invalid title for route "${
scene.route.routeName
}" - title must be string or null, instead it was of type ${typeof options.title}`
);
}
return options.title;
}
_getLastScene(scene) {
return this.props.scenes.find(s => s.index === scene.index - 1);
}
_getBackButtonTitleString(scene) {
const lastScene = this._getLastScene(scene);
if (!lastScene) {
return null;
}
const { headerBackTitle } = lastScene.descriptor.options;
if (headerBackTitle || headerBackTitle === null) {
return headerBackTitle;
}
return this._getHeaderTitleString(lastScene);
}
_getTruncatedBackButtonTitle(scene) {
const lastScene = this._getLastScene(scene);
if (!lastScene) {
return null;
}
return lastScene.descriptor.options.headerTruncatedBackTitle;
}
_renderTitleComponent = props => {
const { layoutPreset } = this.props;
const { options } = props.scene.descriptor;
const headerTitle = 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;
// When title is centered, the width of left/right components depends on the
// calculated size of the title.
const onLayout =
layoutPreset === 'center'
? e => {
this.setState({
widths: {
...this.state.widths,
[props.scene.key]: e.nativeEvent.layout.width,
},
});
}
: undefined;
const HeaderTitleComponent =
headerTitle && typeof headerTitle !== 'string'
? headerTitle
: HeaderTitle;
return (
<HeaderTitleComponent
onLayout={onLayout}
allowFontScaling={allowFontScaling == null ? true : allowFontScaling}
style={[
color ? { color } : null,
layoutPreset === 'center'
? { textAlign: 'center' }
: { textAlign: 'left' },
titleStyle,
]}
>
{titleString}
</HeaderTitleComponent>
);
};
_renderLeftComponent = props => {
const { options } = props.scene.descriptor;
if (
React.isValidElement(options.headerLeft) ||
options.headerLeft === null
) {
return options.headerLeft;
}
if (!options.headerLeft && props.scene.index === 0) {
return;
}
const backButtonTitle = this._getBackButtonTitleString(props.scene);
const truncatedBackButtonTitle = this._getTruncatedBackButtonTitle(
props.scene
);
const width = this.state.widths[props.scene.key]
? (this.props.layout.initWidth - this.state.widths[props.scene.key]) / 2
: undefined;
const RenderedLeftComponent = options.headerLeft || HeaderBackButton;
const goBack = () => {
// Go back on next tick because button ripple effect needs to happen on Android
requestAnimationFrame(() => {
props.scene.descriptor.navigation.goBack(props.scene.descriptor.key);
});
};
return (
<RenderedLeftComponent
onPress={goBack}
pressColorAndroid={options.headerPressColorAndroid}
tintColor={options.headerTintColor}
backImage={options.headerBackImage}
title={backButtonTitle}
truncatedTitle={truncatedBackButtonTitle}
backTitleVisible={this.props.backTitleVisible}
titleStyle={options.headerBackTitleStyle}
layoutPreset={this.props.layoutPreset}
width={width}
/>
);
};
_renderModularLeftComponent = (
props,
ButtonContainerComponent,
LabelContainerComponent
) => {
const { options, navigation } = props.scene.descriptor;
const backButtonTitle = this._getBackButtonTitleString(props.scene);
const truncatedBackButtonTitle = this._getTruncatedBackButtonTitle(
props.scene
);
const width = this.state.widths[props.scene.key]
? (this.props.layout.initWidth - this.state.widths[props.scene.key]) / 2
: undefined;
const goBack = () => {
// Go back on next tick because button ripple effect needs to happen on Android
requestAnimationFrame(() => {
navigation.goBack(props.scene.descriptor.key);
});
};
return (
<ModularHeaderBackButton
onPress={goBack}
ButtonContainerComponent={ButtonContainerComponent}
LabelContainerComponent={LabelContainerComponent}
pressColorAndroid={options.headerPressColorAndroid}
tintColor={options.headerTintColor}
backImage={options.headerBackImage}
title={backButtonTitle}
truncatedTitle={truncatedBackButtonTitle}
titleStyle={options.headerBackTitleStyle}
width={width}
/>
);
};
_renderRightComponent = props => {
const { headerRight } = props.scene.descriptor.options;
return headerRight || null;
};
_renderLeft(props) {
const { options } = props.scene.descriptor;
const { transitionPreset } = this.props;
let { style } = props;
if (options.headerLeftContainerStyle) {
style = [style, options.headerLeftContainerStyle];
}
// 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, style },
'left',
this._renderLeftComponent,
this.props.leftInterpolator
);
} else {
return this._renderModularSubView(
{ ...props, style },
'left',
this._renderModularLeftComponent,
this.props.leftLabelInterpolator,
this.props.leftButtonInterpolator
);
}
}
_renderTitle(props, options) {
const { layoutPreset, transitionPreset } = this.props;
let style = [
{ justifyContent: layoutPreset === 'center' ? 'center' : 'flex-start' },
getTitleOffsets(
layoutPreset,
false,
options.hasLeftComponent,
options.hasRightComponent
),
options.headerTitleContainerStyle,
];
return this._renderSubView(
{ ...props, style },
'title',
this._renderTitleComponent,
transitionPreset === 'uikit'
? this.props.titleFromLeftInterpolator
: this.props.titleInterpolator
);
}
_renderRight(props) {
const { options } = props.scene.descriptor;
let { style } = props;
if (options.headerRightContainerStyle) {
style = [style, options.headerRightContainerStyle];
}
return this._renderSubView(
{ ...props, style },
'right',
this._renderRightComponent,
this.props.rightInterpolator
);
}
_renderModularSubView(
props,
name,
renderer,
labelStyleInterpolator,
buttonStyleInterpolator
) {
const { scene } = props;
const { index, isStale, key } = scene;
// Never render a modular back button on the first screen in a stack.
if (index === 0) {
return;
}
const offset = this.props.navigation.state.index - index;
if (Math.abs(offset) > 2) {
// Scene is far away from the active scene. Hides it to avoid unnecessary
// rendering.
return null;
}
const ButtonContainer = ({ children }) => (
<Animated.View
style={[buttonStyleInterpolator({ ...this.props, ...props })]}
>
{children}
</Animated.View>
);
const LabelContainer = ({ children }) => (
<Animated.View
style={[labelStyleInterpolator({ ...this.props, ...props })]}
>
{children}
</Animated.View>
);
const subView = renderer(props, ButtonContainer, LabelContainer);
if (subView === null) {
return subView;
}
const pointerEvents = offset !== 0 || isStale ? 'none' : 'box-none';
return (
<View
key={`${name}_${key}`}
pointerEvents={pointerEvents}
style={[styles.item, styles[name], props.style]}
>
{subView}
</View>
);
}
_renderSubView(props, name, renderer, styleInterpolator) {
const { scene } = props;
const { index, isStale, key } = scene;
const offset = this.props.navigation.state.index - index;
if (Math.abs(offset) > 2) {
// Scene is far away from the active scene. Hides it to avoid unnecessary
// rendering.
return null;
}
const subView = renderer(props);
if (subView == null) {
return null;
}
const pointerEvents = offset !== 0 || isStale ? 'none' : 'box-none';
return (
<Animated.View
pointerEvents={pointerEvents}
key={`${name}_${key}`}
style={[
styles.item,
styles[name],
props.style,
styleInterpolator({
...this.props,
...props,
}),
]}
>
{subView}
</Animated.View>
);
}
_renderHeader(props) {
const { options } = props.scene.descriptor;
if (options.header === null) {
return null;
}
const left = this._renderLeft(props);
const right = this._renderRight(props);
const title = this._renderTitle(props, {
hasLeftComponent: !!left,
hasRightComponent: !!right,
headerTitleContainerStyle: options.headerTitleContainerStyle,
});
const { isLandscape, transitionPreset } = this.props;
const wrapperProps = {
style: styles.header,
key: `scene_${props.scene.key}`,
};
if (
options.headerLeft ||
options.headerBackImage ||
Platform.OS !== 'ios' ||
transitionPreset !== 'uikit'
) {
return (
<View {...wrapperProps}>
{title}
{left}
{right}
</View>
);
} else {
return (
<MaskedViewIOS
{...wrapperProps}
maskElement={
<View style={styles.iconMaskContainer}>
<Image
source={require('../assets/back-icon-mask.png')}
style={styles.iconMask}
/>
<View style={styles.iconMaskFillerRect} />
</View>
}
>
{title}
{left}
{right}
</MaskedViewIOS>
);
}
}
render() {
let appBar;
const { mode, scene, isLandscape } = this.props;
if (mode === 'float') {
const scenesByIndex = {};
this.props.scenes.forEach(scene => {
scenesByIndex[scene.index] = scene;
});
const scenesProps = Object.values(scenesByIndex).map(scene => ({
position: this.props.position,
progress: this.props.progress,
scene,
}));
appBar = scenesProps.map(this._renderHeader, this);
} else {
appBar = this._renderHeader({
position: new Animated.Value(this.props.scene.index),
progress: new Animated.Value(0),
scene: this.props.scene,
});
}
const { options } = scene.descriptor;
const { headerStyle = {} } = options;
const headerStyleObj = StyleSheet.flatten(headerStyle);
const appBarHeight = getAppBarHeight(isLandscape);
const {
alignItems,
justifyContent,
flex,
flexDirection,
flexGrow,
flexShrink,
flexBasis,
flexWrap,
position,
padding,
paddingHorizontal,
paddingRight,
paddingLeft,
// paddingVertical,
// paddingTop,
// paddingBottom,
top,
right,
bottom,
left,
...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');
warnIfHeaderStyleDefined(padding, 'padding');
warnIfHeaderStyleDefined(position, 'position');
warnIfHeaderStyleDefined(paddingHorizontal, 'paddingHorizontal');
warnIfHeaderStyleDefined(paddingRight, 'paddingRight');
warnIfHeaderStyleDefined(paddingLeft, 'paddingLeft');
// warnIfHeaderStyleDefined(paddingVertical, 'paddingVertical');
// warnIfHeaderStyleDefined(paddingTop, 'paddingTop');
// warnIfHeaderStyleDefined(paddingBottom, 'paddingBottom');
warnIfHeaderStyleDefined(top, 'top');
warnIfHeaderStyleDefined(right, 'right');
warnIfHeaderStyleDefined(bottom, 'bottom');
warnIfHeaderStyleDefined(left, 'left');
}
// 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 (
<Animated.View
style={[
this.props.layoutInterpolator(this.props),
Platform.OS === 'ios' && !options.headerTransparent
? {
backgroundColor:
safeHeaderStyle.backgroundColor || DEFAULT_BACKGROUND_COLOR,
}
: null,
]}
>
<SafeAreaView forceInset={forceInset} style={containerStyles}>
<View style={StyleSheet.absoluteFill}>
{options.headerBackground}
</View>
<View style={styles.flexOne}>{appBar}</View>
</SafeAreaView>
</Animated.View>
);
}
}
function warnIfHeaderStyleDefined(value, styleProp) {
if (styleProp === 'position' && value === 'absolute') {
console.warn(
"position: 'absolute' is not supported on headerStyle. If you would like to render content under the header, use the headerTransparent navigationOption."
);
} else 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 DEFAULT_BACKGROUND_COLOR = Platform.OS === 'ios' ? '#F7F7F7' : '#FFF';
const styles = StyleSheet.create({
container: {
backgroundColor: DEFAULT_BACKGROUND_COLOR,
...platformContainerStyles,
},
transparentContainer: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
...platformContainerStyles,
borderBottomWidth: 0,
borderBottomColor: 'transparent',
elevation: 0,
},
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',
transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }],
},
title: {
bottom: 0,
top: 0,
position: 'absolute',
alignItems: 'center',
flexDirection: 'row',
},
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',
},
flexOne: {
flex: 1,
},
});
export default withOrientation(Header);

View File

@@ -1,175 +0,0 @@
import React from 'react';
import {
I18nManager,
Image,
Text,
View,
Platform,
StyleSheet,
} from 'react-native';
import TouchableItem from '../TouchableItem';
const defaultBackImage = require('../assets/back-icon.png');
class HeaderBackButton extends React.PureComponent {
static defaultProps = {
pressColorAndroid: 'rgba(0, 0, 0, .32)',
tintColor: Platform.select({
ios: '#037aff',
}),
truncatedTitle: 'Back',
};
state = {};
_onTextLayout = e => {
if (this.state.initialTextWidth) {
return;
}
this.setState({
initialTextWidth: e.nativeEvent.layout.x + e.nativeEvent.layout.width,
});
};
_renderBackImage() {
const { backImage, title, tintColor } = this.props;
let BackImage;
let props;
if (React.isValidElement(backImage)) {
return backImage;
} else if (backImage) {
BackImage = backImage;
props = {
tintColor,
title,
};
} else {
BackImage = Image;
props = {
style: [
styles.icon,
!!title && styles.iconWithTitle,
!!tintColor && { tintColor },
],
source: defaultBackImage,
};
}
return <BackImage {...props} />;
}
render() {
const { onPress, pressColorAndroid, layoutPreset, title } = this.props;
let button = (
<TouchableItem
accessibilityComponentType="button"
accessibilityLabel={title}
accessibilityTraits="button"
testID="header-back"
delayPressIn={0}
onPress={onPress}
pressColor={pressColorAndroid}
style={styles.container}
borderless
>
<View style={styles.container}>
{this._renderBackImage()}
{this._maybeRenderTitle()}
</View>
</TouchableItem>
);
if (Platform.OS === 'android') {
return <View style={styles.androidButtonWrapper}>{button}</View>;
} else {
return button;
}
}
_maybeRenderTitle() {
const {
layoutPreset,
backTitleVisible,
width,
title,
titleStyle,
tintColor,
truncatedTitle,
} = this.props;
const renderTruncated =
this.state.initialTextWidth && width
? this.state.initialTextWidth > width
: false;
const backButtonTitle = renderTruncated ? truncatedTitle : title;
// If the left preset is used and we aren't on Android, then we
// default to disabling the label
const titleDefaultsToDisabled =
layoutPreset === 'left' ||
Platform.OS === 'android' ||
typeof backButtonTitle !== 'string';
// If the title is explicitly enabled then we respect that
if (titleDefaultsToDisabled && !backTitleVisible) {
return null;
}
return (
<Text
onLayout={this._onTextLayout}
style={[styles.title, !!tintColor && { color: tintColor }, titleStyle]}
numberOfLines={1}
>
{backButtonTitle}
</Text>
);
}
}
const styles = StyleSheet.create({
androidButtonWrapper: {
margin: 13,
backgroundColor: 'transparent',
},
container: {
alignItems: 'center',
flexDirection: 'row',
backgroundColor: 'transparent',
},
title: {
fontSize: 17,
paddingRight: 10,
},
icon:
Platform.OS === 'ios'
? {
height: 21,
width: 13,
marginLeft: 9,
marginRight: 22,
marginVertical: 12,
resizeMode: 'contain',
transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }],
}
: {
height: 24,
width: 24,
margin: 3,
resizeMode: 'contain',
transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }],
},
iconWithTitle:
Platform.OS === 'ios'
? {
marginRight: 6,
}
: {},
});
export default HeaderBackButton;

View File

@@ -1,336 +0,0 @@
import { Dimensions, I18nManager } from 'react-native';
import getSceneIndicesForInterpolationInputRange from '../../utils/getSceneIndicesForInterpolationInputRange';
function hasHeader(scene) {
if (!scene) {
return true;
}
const { descriptor } = scene;
return descriptor.options.header !== null;
}
const crossFadeInterpolation = (scenes, first, index, last) => ({
inputRange: [
first,
first + 0.001,
index - 0.9,
index - 0.2,
index,
last - 0.001,
last,
],
outputRange: [
0,
hasHeader(scenes[first]) ? 0 : 1,
hasHeader(scenes[first]) ? 0 : 1,
hasHeader(scenes[first]) ? 0.3 : 1,
hasHeader(scenes[index]) ? 1 : 0,
hasHeader(scenes[last]) ? 0 : 1,
0,
],
});
/**
* Utilities that build the style for the navigation header.
*
* +-------------+-------------+-------------+
* | | | |
* | Left | Title | Right |
* | Component | Component | Component |
* | | | |
* +-------------+-------------+-------------+
*/
function isGoingBack(scenes) {
const lastSceneIndexInScenes = scenes.length - 1;
return !scenes[lastSceneIndexInScenes].isActive;
}
function forLayout(props) {
const { layout, position, scene, scenes, mode } = props;
if (mode !== 'float') {
return {};
}
const isBack = isGoingBack(scenes);
const interpolate = getSceneIndicesForInterpolationInputRange(props);
if (!interpolate) return {};
const { first, last } = interpolate;
const index = scene.index;
// We really shouldn't render the scene at all until we know the width of the
// stack. That said, in every case that I have ever seen, this has just been
// the full width of the window. This won't continue to be true if we support
// layouts like iPad master-detail. For now, in order to solve
// https://github.com/react-navigation/react-navigation/issues/4264, I have
// opted for the heuristic that we will use the window width until we have
// measured (and they will usually be the same).
const width = layout.initWidth || Dimensions.get('window').width;
// Make sure the header stays hidden when transitioning between 2 screens
// with no header.
if (
(isBack && !hasHeader(scenes[index]) && !hasHeader(scenes[last])) ||
(!isBack && !hasHeader(scenes[first]) && !hasHeader(scenes[index]))
) {
return {
transform: [{ translateX: width }],
};
}
const rtlMult = I18nManager.isRTL ? -1 : 1;
const translateX = position.interpolate({
inputRange: [first, index, last],
outputRange: [
rtlMult * (hasHeader(scenes[first]) ? 0 : width),
rtlMult * (hasHeader(scenes[index]) ? 0 : isBack ? width : -width),
rtlMult * (hasHeader(scenes[last]) ? 0 : -width),
],
});
return {
transform: [{ translateX }],
};
}
function forLeft(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(
crossFadeInterpolation(scenes, first, index, last)
),
};
}
function forCenter(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(
crossFadeInterpolation(scenes, first, index, last)
),
};
}
function forRight(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(
crossFadeInterpolation(scenes, 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;
// The gist of what we're doing here is animating the left button _normally_ (fast fade)
// when both scenes in transition have headers. When the current, next, or previous scene _don't_
// have a header, we don't fade the button, and only set it's opacity to 0 at the last moment
// of the transition.
const inputRange = [
first,
first + 0.001,
first + Math.abs(index - first) / 2,
index,
last - Math.abs(last - index) / 2,
last - 0.001,
last,
];
const outputRange = [
0,
hasHeader(scenes[first]) ? 0 : 1,
hasHeader(scenes[first]) ? 0.1 : 1,
hasHeader(scenes[index]) ? 1 : 0,
hasHeader(scenes[last]) ? 0.1 : 1,
hasHeader(scenes[last]) ? 0 : 1,
0,
];
return {
opacity: position.interpolate({
inputRange,
outputRange,
}),
};
}
/*
* NOTE: this offset calculation is 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;
// Similarly to the animation of the left label, when animating to or from a scene without
// a header, we keep the label at full opacity and in the same position for as long as possible.
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,
first + 0.001,
index - 0.35,
index,
index + 0.5,
last - 0.001,
last,
],
outputRange: [
0,
hasHeader(scenes[first]) ? 0 : 1,
hasHeader(scenes[first]) ? 0 : 1,
hasHeader(scenes[index]) ? 1 : 0,
hasHeader(scenes[last]) ? 0.5 : 1,
hasHeader(scenes[last]) ? 0 : 1,
0,
],
}),
transform: [
{
translateX: position.interpolate({
inputRange: [first, first + 0.001, index, last - 0.001, last],
outputRange: I18nManager.isRTL
? [
-offset * 1.5,
hasHeader(scenes[first]) ? -offset * 1.5 : 0,
0,
hasHeader(scenes[last]) ? offset : 0,
offset,
]
: [
offset,
hasHeader(scenes[first]) ? offset : 0,
0,
hasHeader(scenes[last]) ? -offset * 1.5 : 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, scenes } = 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,
first + 0.001,
index - 0.5,
index,
index + 0.7,
last - 0.001,
last,
],
outputRange: [
0,
hasHeader(scenes[first]) ? 0 : 1,
hasHeader(scenes[first]) ? 0 : 1,
hasHeader(scenes[index]) ? 1 : 0,
hasHeader(scenes[last]) ? 0 : 1,
hasHeader(scenes[last]) ? 0 : 1,
0,
],
}),
transform: [
{
translateX: position.interpolate({
inputRange: [first, first + 0.001, index, last - 0.001, last],
outputRange: I18nManager.isRTL
? [
-offset,
hasHeader(scenes[first]) ? -offset : 0,
0,
hasHeader(scenes[last]) ? offset : 0,
offset,
]
: [
offset,
hasHeader(scenes[first]) ? offset : 0,
0,
hasHeader(scenes[last]) ? -offset : 0,
-offset,
],
}),
},
],
};
}
export default {
forLayout,
forLeft,
forLeftButton,
forLeftLabel,
forCenterFromLeft,
forCenter,
forRight,
};

View File

@@ -1,24 +0,0 @@
import React from 'react';
import { Text, View, Platform, StyleSheet, Animated } from 'react-native';
const AnimatedText = Animated.Text;
const HeaderTitle = ({ style, ...rest }) => (
<AnimatedText
numberOfLines={1}
{...rest}
style={[styles.title, style]}
accessibilityTraits="header"
/>
);
const styles = StyleSheet.create({
title: {
fontSize: Platform.OS === 'ios' ? 17 : 20,
fontWeight: Platform.OS === 'ios' ? '700' : '500',
color: 'rgba(0, 0, 0, .9)',
marginHorizontal: 16,
},
});
export default HeaderTitle;

View File

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

View File

@@ -1,209 +0,0 @@
import invariant from '../utils/invariant';
import shallowEqual from '../utils/shallowEqual';
const SCENE_KEY_PREFIX = 'scene_';
/**
* Helper function to compare route keys (e.g. "9", "11").
*/
function compareKey(one, two) {
const delta = one.length - two.length;
if (delta > 0) {
return 1;
}
if (delta < 0) {
return -1;
}
return one > two ? 1 : -1;
}
/**
* Helper function to sort scenes based on their index and view key.
*/
function compareScenes(one, two) {
if (one.index > two.index) {
return 1;
}
if (one.index < two.index) {
return -1;
}
return compareKey(one.key, two.key);
}
/**
* Whether two routes are the same.
*/
function areScenesShallowEqual(one, two) {
return (
one.key === two.key &&
one.index === two.index &&
one.isStale === two.isStale &&
one.isActive === two.isActive &&
areRoutesShallowEqual(one.route, two.route)
);
}
/**
* Whether two routes are the same.
*/
function areRoutesShallowEqual(one, two) {
if (!one || !two) {
return one === two;
}
if (one.key !== two.key) {
return false;
}
return shallowEqual(one, two);
}
export default function ScenesReducer(
scenes,
nextState,
prevState,
descriptors
) {
// Always update the descriptors
// This is a workaround for https://github.com/react-navigation/react-navigation/issues/4271
// It will be resolved in a better way when we re-write Transitioner
scenes.forEach(scene => {
const { route } = scene;
if (descriptors && descriptors[route.key]) {
scene.descriptor = descriptors[route.key];
}
});
// Bail out early if we didn't update the state
if (prevState === nextState) {
return scenes;
}
const prevScenes = new Map();
const freshScenes = new Map();
const staleScenes = new Map();
// Populate stale scenes from previous scenes marked as stale.
scenes.forEach(scene => {
const { key } = scene;
if (scene.isStale) {
staleScenes.set(key, scene);
}
prevScenes.set(key, scene);
});
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),
`navigation.state.routes[${index}].key "${key}" conflicts with ` +
'another route!'
);
nextKeys.add(key);
if (staleScenes.has(key)) {
// A previously `stale` scene is now part of the nextState, so we
// revive it by removing it from the stale scene map.
staleScenes.delete(key);
}
freshScenes.set(key, scene);
});
if (prevState) {
// Look at the previous routes and classify any removed scenes as `stale`.
prevState.routes.forEach((route, index) => {
const key = SCENE_KEY_PREFIX + route.key;
if (freshScenes.has(key)) {
return;
}
const lastScene = scenes.find(scene => scene.route.key === route.key);
// We can get into a weird place where we have a queued transition and then clobber
// that transition without ever actually rendering the scene, in which case
// there is no lastScene. If the descriptor is not available on the lastScene
// or the descriptors prop then we just skip adding it to stale scenes and it's
// not ever rendered.
const descriptor = lastScene
? lastScene.descriptor
: descriptors[route.key];
if (descriptor) {
staleScenes.set(key, {
index,
isActive: false,
isStale: true,
key,
route,
descriptor,
});
}
});
}
const nextScenes = [];
const mergeScene = nextScene => {
const { key } = nextScene;
const prevScene = prevScenes.has(key) ? prevScenes.get(key) : null;
if (prevScene && areScenesShallowEqual(prevScene, nextScene)) {
// Reuse `prevScene` as `scene` so view can avoid unnecessary re-render.
// This assumes that the scene's navigation state is immutable.
nextScenes.push(prevScene);
} else {
nextScenes.push(nextScene);
}
};
staleScenes.forEach(mergeScene);
freshScenes.forEach(mergeScene);
nextScenes.sort(compareScenes);
let activeScenesCount = 0;
nextScenes.forEach((scene, ii) => {
const isActive = !scene.isStale && scene.index === nextState.index;
if (isActive !== scene.isActive) {
nextScenes[ii] = {
...scene,
isActive,
};
}
if (isActive) {
activeScenesCount++;
}
});
invariant(
activeScenesCount === 1,
'there should always be only one scene active, not %s.',
activeScenesCount
);
if (nextScenes.length !== scenes.length) {
return nextScenes;
}
if (
nextScenes.some(
(scene, index) => !areScenesShallowEqual(scenes[index], scene)
)
) {
return nextScenes;
}
// scenes haven't changed.
return scenes;
}

View File

@@ -1,77 +0,0 @@
import React from 'react';
import { NativeModules } from 'react-native';
import StackViewLayout from './StackViewLayout';
import Transitioner from '../Transitioner';
import StackActions from '../../routers/StackActions';
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}
screenProps={this.props.screenProps}
navigation={this.props.navigation}
descriptors={this.props.descriptors}
onTransitionStart={
this.props.onTransitionStart ||
this.props.navigationConfig.onTransitionStart
}
onTransitionEnd={(transition, lastTransition) => {
const { navigationConfig, navigation } = this.props;
const onTransitionEnd =
this.props.onTransitionEnd || navigationConfig.onTransitionEnd;
if (transition.navigation.state.isTransitioning) {
navigation.dispatch(
StackActions.completeTransition({
key: navigation.state.key,
})
);
}
onTransitionEnd && onTransitionEnd(transition, lastTransition);
}}
/>
);
}
_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}
onGestureBegin={this.props.onGestureBegin}
onGestureCanceled={this.props.onGestureCanceled}
onGestureEnd={this.props.onGestureEnd}
screenProps={screenProps}
descriptors={this.props.descriptors}
transitionProps={transitionProps}
lastTransitionProps={lastTransitionProps}
/>
);
};
}
export default StackView;

View File

@@ -1,36 +0,0 @@
import React from 'react';
import { Animated, StyleSheet } from 'react-native';
import createPointerEventsContainer from './createPointerEventsContainer';
/**
* Component that renders the scene as card for the <StackView />.
*/
class Card extends React.Component {
render() {
const { children, pointerEvents, style } = this.props;
return (
<Animated.View
pointerEvents={pointerEvents}
ref={this.props.onComponentRef}
style={[styles.main, style]}
>
{children}
</Animated.View>
);
}
}
const styles = StyleSheet.create({
main: {
...StyleSheet.absoluteFillObject,
backgroundColor: '#E9E9EF',
shadowColor: 'black',
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0.2,
shadowRadius: 5,
},
});
Card = createPointerEventsContainer(Card);
export default Card;

View File

@@ -1,638 +0,0 @@
import React from 'react';
import clamp from 'clamp';
import {
Animated,
StyleSheet,
PanResponder,
Platform,
View,
I18nManager,
Easing,
Dimensions,
} from 'react-native';
import Card from './StackViewCard';
import Header from '../Header/Header';
import NavigationActions from '../../NavigationActions';
import StackActions from '../../routers/StackActions';
import SceneView from '../SceneView';
import withOrientation from '../withOrientation';
import { NavigationProvider } from '../NavigationContext';
import TransitionConfigs from './StackViewTransitionConfigs';
import { supportsImprovedSpringAnimation } from '../../utils/ReactNativeFeatures';
const emptyFunction = () => {};
const { width: WINDOW_WIDTH, height: WINDOW_HEIGHT } = Dimensions.get('window');
const IS_IPHONE_X =
Platform.OS === 'ios' &&
!Platform.isPad &&
!Platform.isTVOS &&
(WINDOW_HEIGHT === 812 || WINDOW_WIDTH === 812);
const EaseInOut = Easing.inOut(Easing.ease);
/**
* Enumerate possible values for validation
*/
const HEADER_LAYOUT_PRESET_VALUES = ['center', 'left'];
const HEADER_TRANSITION_PRESET_VALUES = ['uikit', 'fade-in-place'];
/**
* 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);
}
};
const getDefaultHeaderHeight = isLandscape => {
if (Platform.OS === 'ios') {
if (isLandscape && !Platform.isPad) {
return 32;
} else if (IS_IPHONE_X) {
return 88;
} else {
return 64;
}
} else {
return 56;
}
};
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;
constructor(props) {
super(props);
this.state = {
// Used when card's header is null and mode is float to make transition
// between screens with headers and those without headers smooth.
// This is not a great heuristic here. We don't know synchronously
// on mount what the header height is so we have just used the most
// common cases here.
floatingHeaderHeight: getDefaultHeaderHeight(props.isLandscape),
};
}
_renderHeader(scene, headerMode) {
const { options } = scene.descriptor;
const { header } = options;
if (__DEV__ && typeof header === 'string') {
throw new Error(
`Invalid header value: "${header}". The header option must be a valid React component or null, not a string.`
);
}
if (header === null && headerMode === 'screen') {
return null;
}
// check if it's a react element
if (React.isValidElement(header)) {
return header;
}
// Handle the case where the header option is a function, and provide the default
const renderHeader = header || (props => <Header {...props} />);
const {
headerLeftInterpolator,
headerTitleInterpolator,
headerRightInterpolator,
} = this._getTransitionConfig();
const {
mode,
transitionProps,
lastTransitionProps,
...passProps
} = this.props;
return (
<NavigationProvider value={scene.descriptor.navigation}>
{renderHeader({
...passProps,
...transitionProps,
scene,
mode: headerMode,
transitionPreset: this._getHeaderTransitionPreset(),
layoutPreset: this._getHeaderLayoutPreset(),
backTitleVisible: this._getheaderBackTitleVisible(),
leftInterpolator: headerLeftInterpolator,
titleInterpolator: headerTitleInterpolator,
rightInterpolator: headerRightInterpolator,
})}
</NavigationProvider>
);
}
// eslint-disable-next-line class-methods-use-this
_animatedSubscribe(props) {
// Hack to make this work with native driven animations. We add a single listener
// so the JS value of the following animated values gets updated. We rely on
// some Animated private APIs and not doing so would require using a bunch of
// value listeners but we'd have to remove them to not leak and I'm not sure
// when we'd do that with the current structure we have. `stopAnimation` callback
// is also broken with native animated values that have no listeners so if we
// want to remove this we have to fix this too.
animatedSubscribeValue(props.transitionProps.layout.width);
animatedSubscribeValue(props.transitionProps.layout.height);
animatedSubscribeValue(props.transitionProps.position);
}
_reset(resetToIndex, duration) {
if (Platform.OS === 'ios' && 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,
})
);
navigation.dispatch(StackActions.completeTransition());
}
};
if (Platform.OS === 'ios' && 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);
}
}
_panResponder = PanResponder.create({
onPanResponderTerminate: () => {
const { navigation } = this.props.transitionProps;
const { index } = navigation.state;
this._isResponding = false;
this._reset(index, 0);
this.props.onGestureCanceled && this.props.onGestureCanceled();
},
onPanResponderGrant: () => {
const {
transitionProps: { navigation, position, scene },
} = this.props;
const { index } = navigation.state;
if (index !== scene.index) {
return false;
}
position.stopAnimation(value => {
this._isResponding = true;
this._gestureStartValue = value;
});
this.props.onGestureBegin && this.props.onGestureBegin();
},
onMoveShouldSetPanResponder: (event, gesture) => {
const {
transitionProps: { navigation, layout, scene },
mode,
} = this.props;
const { index } = navigation.state;
const isVertical = mode === 'modal';
const { options } = scene.descriptor;
const gestureDirection = options.gestureDirection;
const gestureDirectionInverted =
typeof gestureDirection === 'string'
? gestureDirection === 'inverted'
: I18nManager.isRTL;
if (index !== scene.index) {
return false;
}
const immediateIndex =
this._immediateIndex == null ? index : this._immediateIndex;
const currentDragDistance = gesture[isVertical ? 'dy' : 'dx'];
const currentDragPosition =
event.nativeEvent[isVertical ? 'pageY' : 'pageX'];
const axisLength = isVertical
? layout.height.__getValue()
: layout.width.__getValue();
const axisHasBeenMeasured = !!axisLength;
// Measure the distance from the touch to the edge of the screen
const screenEdgeDistance = gestureDirectionInverted
? axisLength - (currentDragPosition - currentDragDistance)
: currentDragPosition - currentDragDistance;
// Compare to the gesture distance relavant to card or modal
const {
gestureResponseDistance: userGestureResponseDistance = {},
} = 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) => {
const {
transitionProps: { navigation, position, layout, scene },
mode,
} = this.props;
const { index } = navigation.state;
const isVertical = mode === 'modal';
const { options } = scene.descriptor;
const gestureDirection = options.gestureDirection;
const gestureDirectionInverted =
typeof gestureDirection === 'string'
? gestureDirection === 'inverted'
: I18nManager.isRTL;
// 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 =
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) => {
const {
transitionProps: { navigation, position, layout, scene },
mode,
} = this.props;
const { index } = navigation.state;
const isVertical = mode === 'modal';
const { options } = scene.descriptor;
const gestureDirection = options.gestureDirection;
const gestureDirectionInverted =
typeof gestureDirection === 'string'
? gestureDirection === 'inverted'
: I18nManager.isRTL;
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.props.onGestureCanceled && this.props.onGestureCanceled();
this._reset(immediateIndex, resetDuration);
return;
}
if (gestureVelocity > 0.5) {
this.props.onGestureFinish && this.props.onGestureFinish();
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.props.onGestureFinish && this.props.onGestureFinish();
this._goBack(immediateIndex, goBackDuration);
} else {
this.props.onGestureCanceled && this.props.onGestureCanceled();
this._reset(immediateIndex, resetDuration);
}
});
},
});
_onFloatingHeaderLayout = e => {
this.setState({ floatingHeaderHeight: e.nativeEvent.layout.height });
};
render() {
let floatingHeader = null;
const headerMode = this._getHeaderMode();
if (headerMode === 'float') {
const { scene } = this.props.transitionProps;
floatingHeader = (
<View pointerEvents="box-none" onLayout={this._onFloatingHeaderLayout}>
{this._renderHeader(scene, headerMode)}
</View>
);
}
const {
transitionProps: { scene, scenes },
mode,
} = this.props;
const { options } = scene.descriptor;
const gesturesEnabled =
typeof options.gesturesEnabled === 'boolean'
? options.gesturesEnabled
: Platform.OS === 'ios';
const responder = !gesturesEnabled ? null : this._panResponder;
const handlers = gesturesEnabled ? responder.panHandlers : {};
const containerStyle = [
styles.container,
this._getTransitionConfig().containerStyle,
];
return (
<View {...handlers} style={containerStyle}>
<View style={styles.scenes}>
{scenes.map(s => this._renderCard(s))}
</View>
{floatingHeader}
</View>
);
}
_getHeaderMode() {
if (this.props.headerMode) {
return this.props.headerMode;
}
if (Platform.OS === 'android' || this.props.mode === 'modal') {
return 'screen';
}
return 'float';
}
_getHeaderLayoutPreset() {
const { headerLayoutPreset } = this.props;
if (headerLayoutPreset) {
if (__DEV__) {
if (
this._getHeaderTransitionPreset() === 'uikit' &&
headerLayoutPreset === 'left' &&
Platform.OS === 'ios'
) {
console.warn(
`headerTransitionPreset with the value 'ui-kit' is incompatible with headerLayoutPreset 'left'`
);
}
}
if (HEADER_LAYOUT_PRESET_VALUES.includes(headerLayoutPreset)) {
return headerLayoutPreset;
}
if (__DEV__) {
console.error(
`Invalid configuration applied for headerLayoutPreset - expected one of ${HEADER_LAYOUT_PRESET_VALUES.join(
', '
)} but received ${JSON.stringify(headerLayoutPreset)}`
);
}
}
if (Platform.OS === 'android') {
return 'left';
} else {
return 'center';
}
}
_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';
}
const { headerTransitionPreset } = this.props;
if (headerTransitionPreset) {
if (HEADER_TRANSITION_PRESET_VALUES.includes(headerTransitionPreset)) {
return headerTransitionPreset;
}
if (__DEV__) {
console.error(
`Invalid configuration applied for headerTransitionPreset - expected one of ${HEADER_TRANSITION_PRESET_VALUES.join(
', '
)} but received ${JSON.stringify(headerTransitionPreset)}`
);
}
}
return 'fade-in-place';
}
_getheaderBackTitleVisible() {
const { headerBackTitleVisible } = this.props;
return headerBackTitleVisible;
}
_renderInnerScene(scene) {
const { options, navigation, getComponent } = scene.descriptor;
const SceneComponent = getComponent();
const { screenProps } = this.props;
const headerMode = this._getHeaderMode();
if (headerMode === 'screen') {
return (
<View style={styles.container}>
<View style={styles.scenes}>
<SceneView
screenProps={screenProps}
navigation={navigation}
component={SceneComponent}
/>
</View>
{this._renderHeader(scene, headerMode)}
</View>
);
}
return (
<SceneView
screenProps={screenProps}
navigation={navigation}
component={SceneComponent}
/>
);
}
_getTransitionConfig = () => {
const isModal = this.props.mode === 'modal';
return TransitionConfigs.getTransitionConfig(
this.props.transitionConfig,
this.props.transitionProps,
this.props.lastTransitionProps,
isModal
);
};
_renderCard = scene => {
const { screenInterpolator } = this._getTransitionConfig();
const style =
screenInterpolator &&
screenInterpolator({ ...this.props.transitionProps, scene });
// If this screen has "header" set to `null` in it's navigation options, but
// it exists in a stack with headerMode float, add a negative margin to
// compensate for the hidden header
const { options } = scene.descriptor;
const hasHeader = options.header !== null;
const headerMode = this._getHeaderMode();
let marginTop = 0;
if (!hasHeader && headerMode === 'float') {
marginTop = -this.state.floatingHeaderHeight;
}
return (
<Card
{...this.props.transitionProps}
key={`card_${scene.key}`}
style={[style, { marginTop }, this.props.cardStyle]}
scene={scene}
>
{this._renderInnerScene(scene)}
</Card>
);
};
}
const styles = StyleSheet.create({
container: {
flex: 1,
// Header is physically rendered after scenes so that Header won't be
// covered by the shadows of the scenes.
// That said, we'd have use `flexDirection: 'column-reverse'` to move
// Header above the scenes.
flexDirection: 'column-reverse',
},
scenes: {
flex: 1,
},
});
export default withOrientation(StackViewLayout);

View File

@@ -1,167 +0,0 @@
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, scene } = props;
const focused = navigation.state.index === scene.index;
const opacity = focused ? 1 : 0;
// If not focused, move the scene 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, 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, first + 0.01, index, last - 0.01, last],
outputRange: [0, 1, 1, 0.85, 0],
});
const width = layout.initWidth;
const translateX = position.interpolate({
inputRange: [first, index, last],
outputRange: I18nManager.isRTL
? [-width, 0, width * 0.3]
: [width, 0, width * -0.3],
});
const translateY = 0;
return {
opacity,
transform: [{ translateX }, { translateY }],
};
}
/**
* Standard iOS-style slide in from the bottom (used for modals).
*/
function forVertical(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, first + 0.01, index, last - 0.01, last],
outputRange: [0, 1, 1, 0.85, 0],
});
const height = layout.initHeight;
const translateY = position.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,
};
}
export default {
forHorizontal,
forVertical,
forFadeFromBottomAndroid,
forFade,
};

View File

@@ -1,114 +0,0 @@
import { Animated, Easing, Platform } from 'react-native';
import StyleInterpolator from './StackViewStyleInterpolator';
import { supportsImprovedSpringAnimation } from '../../utils/ReactNativeFeatures';
let IOSTransitionSpec;
if (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,
prevTransitionProps,
isModal
) {
if (Platform.OS === 'android') {
// Use the default Android animation no matter if the screen is a modal.
// Android doesn't have full-screen modals like iOS does, it has dialogs.
if (
prevTransitionProps &&
transitionProps.index < prevTransitionProps.index
) {
// Navigating back to the previous screen
return FadeOutToBottomAndroid;
}
return FadeInFromBottomAndroid;
}
// iOS and other platforms
if (isModal) {
return ModalSlideFromBottomIOS;
}
return SlideFromRightIOS;
}
function getTransitionConfig(
transitionConfigurer,
transitionProps,
prevTransitionProps,
isModal
) {
const defaultConfig = defaultTransitionConfig(
transitionProps,
prevTransitionProps,
isModal
);
if (transitionConfigurer) {
return {
...defaultConfig,
...transitionConfigurer(transitionProps, prevTransitionProps, isModal),
};
}
return defaultConfig;
}
export default {
defaultTransitionConfig,
getTransitionConfig,
SlideFromRightIOS,
ModalSlideFromBottomIOS,
FadeInFromBottomAndroid,
FadeOutToBottomAndroid,
};

View File

@@ -1,84 +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 {
constructor(props, context) {
super(props, context);
this._pointerEvents = this._computePointerEvents();
}
componentWillUnmount() {
this._positionListener && this._positionListener.remove();
}
render() {
this._bindPosition();
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() {
this._positionListener && this._positionListener.remove();
this._positionListener = new AnimatedValueSubscription(
this.props.position,
this._onPositionChange
);
}
_onPositionChange = () => {
if (this._component) {
const pointerEvents = this._computePointerEvents();
if (this._pointerEvents !== pointerEvents) {
this._pointerEvents = pointerEvents;
this._component.setNativeProps({ pointerEvents });
}
}
};
_computePointerEvents() {
const { navigation, position, scene } = this.props;
if (scene.isStale || navigation.state.index !== scene.index) {
// The scene isn't focused.
return scene.index > navigation.state.index ? 'box-only' : 'none';
}
const offset = position.__getAnimatedValue() - navigation.state.index;
if (Math.abs(offset) > MIN_POSITION_OFFSET) {
// The positon is still away from scene's index.
// Scene's children should not receive touches until the position
// is close enough to scene's index.
return 'box-only';
}
return 'auto';
}
}
return Container;
}

View File

@@ -1,286 +0,0 @@
import React from 'react';
import { Animated, Easing, StyleSheet, View } from 'react-native';
import invariant from '../utils/invariant';
import NavigationScenesReducer from './ScenesReducer';
// Used for all animations unless overriden
const DefaultTransitionSpec = {
duration: 250,
easing: Easing.inOut(Easing.ease),
timing: Animated.timing,
};
class Transitioner extends React.Component {
constructor(props, context) {
super(props, context);
// The initial layout isn't measured. Measured layout will be only available
// when the component is mounted.
const layout = {
height: new Animated.Value(0),
initHeight: 0,
initWidth: 0,
isMeasured: false,
width: new Animated.Value(0),
};
this.state = {
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
),
};
this._prevTransitionProps = null;
this._transitionProps = buildTransitionProps(props, this.state);
this._isMounted = false;
this._isTransitionRunning = false;
this._queuedTransition = null;
}
componentDidMount() {
this._isMounted = true;
}
componentWillUnmount() {
this._isMounted = false;
}
componentWillReceiveProps(nextProps) {
let nextScenes = NavigationScenesReducer(
this.state.scenes,
nextProps.navigation.state,
this.props.navigation.state,
nextProps.descriptors
);
if (!nextProps.navigation.state.isTransitioning) {
nextScenes = filterStale(nextScenes);
}
// Update nextScenes when we change screenProps
// This is a workaround for https://github.com/react-navigation/react-navigation/issues/4271
if (nextProps.screenProps !== this.props.screenProps) {
this.setState({ nextScenes });
}
if (nextScenes === this.state.scenes) {
return;
}
const indexHasChanged =
nextProps.navigation.state.index !== this.props.navigation.state.index;
if (this._isTransitionRunning) {
this._queuedTransition = { nextProps, nextScenes, indexHasChanged };
return;
}
this._startTransition(nextProps, nextScenes, indexHasChanged);
}
_startTransition(nextProps, nextScenes, indexHasChanged) {
const nextState = {
...this.state,
scenes: nextScenes,
};
const { position, progress } = nextState;
progress.setValue(0);
this._prevTransitionProps = this._transitionProps;
this._transitionProps = buildTransitionProps(nextProps, nextState);
const toValue = nextProps.navigation.state.index;
if (!this._transitionProps.navigation.state.isTransitioning) {
this.setState(nextState, async () => {
const result = nextProps.onTransitionStart(
this._transitionProps,
this._prevTransitionProps
);
if (result instanceof Promise) {
await result;
}
progress.setValue(1);
position.setValue(toValue);
this._onTransitionEnd();
});
return;
}
// get the transition spec.
const transitionUserSpec = nextProps.configureTransition
? nextProps.configureTransition(
this._transitionProps,
this._prevTransitionProps
)
: null;
const transitionSpec = {
...DefaultTransitionSpec,
...transitionUserSpec,
};
const { timing } = transitionSpec;
delete transitionSpec.timing;
const positionHasChanged = position.__getValue() !== toValue;
// if swiped back, indexHasChanged == true && positionHasChanged == false
const animations =
indexHasChanged && positionHasChanged
? [
timing(progress, {
...transitionSpec,
toValue: 1,
}),
timing(position, {
...transitionSpec,
toValue: nextProps.navigation.state.index,
}),
]
: [];
// update scenes and play the transition
this._isTransitionRunning = true;
this.setState(nextState, async () => {
if (nextProps.onTransitionStart) {
const result = nextProps.onTransitionStart(
this._transitionProps,
this._prevTransitionProps
);
if (result instanceof Promise) {
await result;
}
}
Animated.parallel(animations).start(this._onTransitionEnd);
});
}
render() {
return (
<View onLayout={this._onLayout} style={styles.main}>
{this.props.render(this._transitionProps, this._prevTransitionProps)}
</View>
);
}
_onLayout = event => {
const { height, width } = event.nativeEvent.layout;
if (
this.state.layout.initWidth === width &&
this.state.layout.initHeight === height
) {
return;
}
const layout = {
...this.state.layout,
initHeight: height,
initWidth: width,
isMeasured: true,
};
layout.height.setValue(height);
layout.width.setValue(width);
const nextState = {
...this.state,
layout,
};
this._transitionProps = buildTransitionProps(this.props, nextState);
this.setState(nextState);
};
_onTransitionEnd = () => {
if (!this._isMounted) {
return;
}
const prevTransitionProps = this._prevTransitionProps;
this._prevTransitionProps = null;
const scenes = filterStale(this.state.scenes);
const nextState = {
...this.state,
scenes,
};
this._transitionProps = buildTransitionProps(this.props, nextState);
this.setState(nextState, async () => {
if (this.props.onTransitionEnd) {
const result = this.props.onTransitionEnd(
this._transitionProps,
prevTransitionProps
);
if (result instanceof Promise) {
await result;
}
}
if (this._queuedTransition) {
this._startTransition(
this._queuedTransition.nextProps,
this._queuedTransition.nextScenes,
this._queuedTransition.indexHasChanged
);
this._queuedTransition = null;
} else {
this._isTransitionRunning = false;
}
});
};
}
function buildTransitionProps(props, state) {
const { navigation } = props;
const { layout, position, progress, scenes } = state;
const scene = scenes.find(isSceneActive);
invariant(scene, 'Could not find active scene');
return {
layout,
navigation,
position,
progress,
scenes,
scene,
index: scene.index,
};
}
function isSceneNotStale(scene) {
return !scene.isStale;
}
function filterStale(scenes) {
const filtered = scenes.filter(isSceneNotStale);
if (filtered.length === scenes.length) {
return scenes;
}
return filtered;
}
function isSceneActive(scene) {
return scene.isActive;
}
const styles = StyleSheet.create({
main: {
flex: 1,
},
});
export default Transitioner;

View File

@@ -1,330 +0,0 @@
import ScenesReducer from '../ScenesReducer';
const MOCK_DESCRIPTOR = {};
/**
* Simulate scenes transtion with changes of navigation states.
*/
function testTransition(states) {
let descriptors = states
.reduce((acc, state) => acc.concat(state), [])
.reduce((acc, key) => {
acc[key] = MOCK_DESCRIPTOR;
return acc;
}, {});
const routes = states.map(keys => ({
index: 0,
routes: keys.map(key => ({ key, routeName: '' })),
isTransitioning: false,
}));
let scenes = [];
let prevState = null;
routes.forEach(nextState => {
scenes = ScenesReducer(scenes, nextState, prevState, descriptors);
prevState = nextState;
});
return scenes;
}
describe('ScenesReducer', () => {
it('gets initial scenes', () => {
const scenes = testTransition([['1', '2']]);
expect(scenes).toEqual([
{
index: 0,
isActive: true,
isStale: false,
descriptor: MOCK_DESCRIPTOR,
key: 'scene_1',
route: {
key: '1',
routeName: '',
},
},
{
index: 1,
isActive: false,
isStale: false,
descriptor: MOCK_DESCRIPTOR,
key: 'scene_2',
route: {
key: '2',
routeName: '',
},
},
]);
});
it('pushes new scenes', () => {
// Transition from ['1', '2'] to ['1', '2', '3'].
const scenes = testTransition([['1', '2'], ['1', '2', '3']]);
expect(scenes).toEqual([
{
index: 0,
isActive: true,
isStale: false,
descriptor: MOCK_DESCRIPTOR,
key: 'scene_1',
route: {
key: '1',
routeName: '',
},
},
{
index: 1,
isActive: false,
isStale: false,
descriptor: MOCK_DESCRIPTOR,
key: 'scene_2',
route: {
key: '2',
routeName: '',
},
},
{
index: 2,
isActive: false,
isStale: false,
descriptor: MOCK_DESCRIPTOR,
key: 'scene_3',
route: {
key: '3',
routeName: '',
},
},
]);
});
it('gets active scene when index changes', () => {
const state1 = {
index: 0,
routes: [{ key: '1', routeName: '' }, { key: '2', routeName: '' }],
isTransitioning: false,
};
const state2 = {
index: 1,
routes: [{ key: '1', routeName: '' }, { key: '2', routeName: '' }],
isTransitioning: false,
};
const scenes1 = ScenesReducer([], state1, null);
const scenes2 = ScenesReducer(scenes1, state2, state1);
const route = scenes2.find(scene => scene.isActive).route;
expect(route).toEqual({ key: '2', routeName: '' });
});
it('gets same scenes', () => {
const state1 = {
index: 0,
routes: [{ key: '1', routeName: '' }, { key: '2', routeName: '' }],
isTransitioning: false,
};
const state2 = {
index: 0,
routes: [{ key: '1', routeName: '' }, { key: '2', routeName: '' }],
isTransitioning: false,
};
const scenes1 = ScenesReducer([], state1, null);
const scenes2 = ScenesReducer(scenes1, state2, state1);
expect(scenes1).toBe(scenes2);
});
it('gets different scenes when keys are different', () => {
const state1 = {
index: 0,
routes: [{ key: '1', routeName: '' }, { key: '2', routeName: '' }],
isTransitioning: false,
};
const state2 = {
index: 0,
routes: [{ key: '2', routeName: '' }, { key: '1', routeName: '' }],
isTransitioning: false,
};
const descriptors = { 1: jest.mock(), 2: jest.mock() };
const scenes1 = ScenesReducer([], state1, null, descriptors);
const scenes2 = ScenesReducer(scenes1, state2, state1, descriptors);
expect(scenes1).not.toBe(scenes2);
});
it('gets different scenes when routes are different', () => {
const state1 = {
index: 0,
routes: [
{ key: '1', x: 1, routeName: '' },
{ key: '2', x: 2, routeName: '' },
],
isTransitioning: false,
};
const state2 = {
index: 0,
routes: [
{ key: '1', x: 3, routeName: '' },
{ key: '2', x: 4, routeName: '' },
],
isTransitioning: false,
};
const descriptors = { 1: MOCK_DESCRIPTOR, 2: MOCK_DESCRIPTOR };
const scenes1 = ScenesReducer([], state1, null, descriptors);
const scenes2 = ScenesReducer(scenes1, state2, state1, descriptors);
expect(scenes1).not.toBe(scenes2);
});
it('gets different scenes when state index changes', () => {
const state1 = {
index: 0,
routes: [
{ key: '1', x: 1, routeName: '' },
{ key: '2', x: 2, routeName: '' },
],
isTransitioning: false,
};
const state2 = {
index: 1,
routes: [
{ key: '1', x: 1, routeName: '' },
{ key: '2', x: 2, routeName: '' },
],
isTransitioning: false,
};
const descriptors = { 1: MOCK_DESCRIPTOR, 2: MOCK_DESCRIPTOR };
const scenes1 = ScenesReducer([], state1, null, descriptors);
const scenes2 = ScenesReducer(scenes1, state2, state1, descriptors);
expect(scenes1).not.toBe(scenes2);
});
it('pops scenes', () => {
// Transition from ['1', '2', '3'] to ['1', '2'].
const scenes = testTransition([['1', '2', '3'], ['1', '2']]);
expect(scenes).toEqual([
{
index: 0,
isActive: true,
isStale: false,
descriptor: MOCK_DESCRIPTOR,
key: 'scene_1',
route: {
key: '1',
routeName: '',
},
},
{
index: 1,
isActive: false,
isStale: false,
descriptor: MOCK_DESCRIPTOR,
key: 'scene_2',
route: {
key: '2',
routeName: '',
},
},
{
index: 2,
isActive: false,
isStale: true,
descriptor: MOCK_DESCRIPTOR,
key: 'scene_3',
route: {
key: '3',
routeName: '',
},
},
]);
});
it('replaces scenes', () => {
const scenes = testTransition([['1', '2'], ['3']]);
expect(scenes).toEqual([
{
index: 0,
isActive: false,
isStale: true,
descriptor: MOCK_DESCRIPTOR,
key: 'scene_1',
route: {
key: '1',
routeName: '',
},
},
{
index: 0,
isActive: true,
isStale: false,
descriptor: MOCK_DESCRIPTOR,
key: 'scene_3',
route: {
key: '3',
routeName: '',
},
},
{
index: 1,
isActive: false,
isStale: true,
descriptor: MOCK_DESCRIPTOR,
key: 'scene_2',
route: {
key: '2',
routeName: '',
},
},
]);
});
it('revives scenes', () => {
const scenes = testTransition([['1', '2'], ['3'], ['2']]);
expect(scenes).toEqual([
{
index: 0,
isActive: false,
isStale: true,
descriptor: MOCK_DESCRIPTOR,
key: 'scene_1',
route: {
key: '1',
routeName: '',
},
},
{
index: 0,
isActive: true,
isStale: false,
descriptor: MOCK_DESCRIPTOR,
key: 'scene_2',
route: {
key: '2',
routeName: '',
},
},
{
index: 0,
isActive: false,
isStale: true,
descriptor: MOCK_DESCRIPTOR,
key: 'scene_3',
route: {
key: '3',
routeName: '',
},
},
]);
});
});

View File

@@ -1,49 +0,0 @@
/* eslint react/display-name:0 */
import React from 'react';
import renderer from 'react-test-renderer';
import Transitioner from '../Transitioner';
describe('Transitioner', () => {
it('should not trigger onTransitionStart and onTransitionEnd when route params are changed', () => {
const onTransitionStartCallback = jest.fn();
const onTransitionEndCallback = jest.fn();
const transitionerProps = {
configureTransition: (transitionProps, prevTransitionProps) => ({}),
navigation: {
state: {
index: 0,
routes: [
{ key: '1', routeName: 'Foo' },
{ key: '2', routeName: 'Bar' },
],
},
goBack: () => false,
dispatch: () => false,
setParams: () => false,
navigate: () => false,
},
render: () => <div />,
onTransitionStart: onTransitionStartCallback,
onTransitionEnd: onTransitionEndCallback,
};
const nextTransitionerProps = {
...transitionerProps,
navigation: {
...transitionerProps.navigation,
state: {
index: 0,
routes: [
{ key: '1', routeName: 'Foo', params: { name: 'Zoom' } },
{ key: '2', routeName: 'Bar' },
],
},
},
};
const component = renderer.create(<Transitioner {...transitionerProps} />);
component.update(<Transitioner {...nextTransitionerProps} />);
expect(onTransitionStartCallback).not.toBeCalled();
expect(onTransitionEndCallback).not.toBeCalled();
});
});

View File

@@ -4751,9 +4751,9 @@ react-native-safe-area-view@^0.7.0:
dependencies:
hoist-non-react-statics "^2.3.1"
react-native-safe-area-view@^0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/react-native-safe-area-view/-/react-native-safe-area-view-0.8.0.tgz#22d78cb8e8658d04a10cd53c1546e0bc86cb7aea"
react-native-safe-area-view@^0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/react-native-safe-area-view/-/react-native-safe-area-view-0.9.0.tgz#10ece2ecac70e7005a5b0b3f06c8833060e6d21f"
dependencies:
hoist-non-react-statics "^2.3.1"
@@ -4848,6 +4848,10 @@ react-navigation-drawer@0.5.0:
dependencies:
react-native-drawer-layout-polyfill "^1.3.2"
react-navigation-stack@0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/react-navigation-stack/-/react-navigation-stack-0.2.3.tgz#9d1e2524aa1d178302c938948b8ece49d713f12b"
react-navigation-tabs@0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/react-navigation-tabs/-/react-navigation-tabs-0.6.0.tgz#2f526194f4360e56c2702e736887449acc2080dc"