diff --git a/examples/NavigationPlayground/js/App.js b/examples/NavigationPlayground/js/App.js
index 62d2e7f22b..1bf3f44822 100644
--- a/examples/NavigationPlayground/js/App.js
+++ b/examples/NavigationPlayground/js/App.js
@@ -19,6 +19,7 @@ import ModalStack from './ModalStack';
import StacksInTabs from './StacksInTabs';
import SimpleStack from './SimpleStack';
import SimpleTabs from './SimpleTabs';
+import Transitions from './Transitions';
const ExampleRoutes = {
SimpleStack: {
@@ -63,6 +64,12 @@ const ExampleRoutes = {
screen: SimpleTabs,
path: 'settings',
},
+ Transitions: {
+ name: 'Custom Transitions',
+ description: 'Custom, complex transitions',
+ screen: Transitions,
+ path: 'transitions',
+ },
};
const MainScreen = ({ navigation }) => (
@@ -103,7 +110,8 @@ const AppNavigator = StackNavigator({
mode: Platform.OS === 'ios' ? 'modal' : 'card',
});
-export default () => ;
+// export default () => ;
+export default Transitions; // TODO change it back
const styles = StyleSheet.create({
item: {
diff --git a/examples/NavigationPlayground/js/Transitions/SharedElements/PhotoDetail.js b/examples/NavigationPlayground/js/Transitions/SharedElements/PhotoDetail.js
new file mode 100644
index 0000000000..4666938332
--- /dev/null
+++ b/examples/NavigationPlayground/js/Transitions/SharedElements/PhotoDetail.js
@@ -0,0 +1,98 @@
+// @flow
+import React, { Component } from 'react';
+import {
+ View,
+ ScrollView,
+ Image,
+ StyleSheet,
+ Animated,
+ Dimensions,
+ Text,
+ Easing,
+ ListView,
+} from 'react-native';
+
+import { Transition } from 'react-navigation';
+
+import Touchable from './Touchable';
+
+const { width: windowWidth } = Dimensions.get("window");
+
+const PhotoDetail = (props) => {
+ const { photo } = props.navigation.state.params;
+ const { url, title, description, image, comments } = photo;
+ const openMoreDetails = photo => props.navigation.navigate('PhotoMoreDetail', { photo });
+ const renderHeader = () => (
+
+
+
+ {title}
+
+ {description}
+
+ );
+ return (
+
+
+
+ )
+};
+
+PhotoDetail.navigationOptions = {
+ title: 'Photo Detail'
+}
+
+const renderComment = ({ author, comment, avatar, time }) => (
+
+
+
+ {author}
+ {time}
+
+ {comment}
+
+
+);
+
+const dsComments = new ListView.DataSource({
+ rowHasChanged: (r1, r2) => r1 !== r2,
+});
+
+
+const styles = StyleSheet.create({
+ image: {
+ width: windowWidth,
+ height: windowWidth / 2,
+ },
+ title: {
+ fontSize: 35,
+ fontWeight: 'bold',
+ },
+ text: {
+ margin: 15,
+ }
+});
+
+const commentStyles = StyleSheet.create({
+ container: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginLeft: 16,
+ marginRight: 16,
+ height: 72,
+ },
+ textContainer: {
+ flex: 1,
+ },
+ text: {
+ },
+ authorContainer: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ }
+});
+
+export default PhotoDetail;
\ No newline at end of file
diff --git a/examples/NavigationPlayground/js/Transitions/SharedElements/PhotoGrid.js b/examples/NavigationPlayground/js/Transitions/SharedElements/PhotoGrid.js
new file mode 100644
index 0000000000..056db70da6
--- /dev/null
+++ b/examples/NavigationPlayground/js/Transitions/SharedElements/PhotoGrid.js
@@ -0,0 +1,103 @@
+// @flow
+import React, { Component } from 'react';
+import {
+ ListView,
+ Image,
+ View,
+ Text,
+ Dimensions,
+ StyleSheet,
+} from 'react-native';
+import faker from 'faker';
+import _ from 'lodash';
+
+import { Transition } from 'react-navigation';
+import Touchable from './Touchable';
+
+const ds = new ListView.DataSource({
+ rowHasChanged: (r1, r2) => r1 !== r2,
+});
+
+const images = [
+ require('./images/img1.jpg'),
+ require('./images/img2.jpg'),
+ require('./images/img3.jpg'),
+ require('./images/img4.jpg'),
+ require('./images/img5.jpg'),
+ require('./images/img6.jpg'),
+ require('./images/img7.jpg'),
+ require('./images/img8.jpg'),
+ require('./images/img9.jpg'),
+ require('./images/img10.jpg'),
+ require('./images/img11.jpg'),
+ require('./images/img12.jpg'),
+]
+
+const photos = Array(50).fill(0).map(_ => ({
+ url: faker.image.animals(500, 500, true),
+ image: images[faker.random.number(images.length - 1)],
+ title: faker.name.findName(),
+ description: faker.lorem.paragraphs(10),
+ comments: Array(20).fill(0).map(_ => (
+ {
+ author: faker.name.findName(),
+ // avatar: avatars[faker.random.number(4)],
+ comment: faker.lorem.sentence(),
+ time: '12/21/2017',
+ }
+ ))
+}));
+
+const colCount = 3;
+
+const { width: windowWidth } = Dimensions.get("window");
+const margin = 2;
+const photoWidth = (windowWidth - margin * colCount * 2) / colCount;
+
+const photoRows = _.chunk(photos, colCount);
+
+class PhotoGrid extends Component {
+ static navigationOptions = {
+ title: 'Photo Grid'
+ }
+ render() {
+ return (
+ );
+ }
+ renderRow(photos) {
+ return (
+
+ {photos.map(this.renderCell.bind(this))}
+
+ )
+ }
+ renderCell(photo) {
+ const onPhotoPressed = photo => this.props.navigation.navigate('PhotoDetail', { photo });
+ return (
+ onPhotoPressed(photo)} key={photo.url}>
+
+
+
+
+ )
+ }
+}
+
+const styles = StyleSheet.create({
+ row: {
+ flexDirection: 'row',
+ },
+ cell: {
+ margin: 2,
+ },
+ image: {
+ width: photoWidth,
+ height: photoWidth,
+ }
+})
+
+export default PhotoGrid;
\ No newline at end of file
diff --git a/examples/NavigationPlayground/js/Transitions/SharedElements/Touchable.js b/examples/NavigationPlayground/js/Transitions/SharedElements/Touchable.js
new file mode 100644
index 0000000000..89e3edc0c9
--- /dev/null
+++ b/examples/NavigationPlayground/js/Transitions/SharedElements/Touchable.js
@@ -0,0 +1,15 @@
+import React from 'react';
+import {
+ TouchableNativeFeedback,
+ TouchableOpacity,
+ Platform,
+} from 'react-native';
+
+const Touchable = props => {
+ const Comp = (Platform.OS === 'android'
+ ? TouchableNativeFeedback
+ : TouchableOpacity);
+ return
+}
+
+export default Touchable;
diff --git a/examples/NavigationPlayground/js/Transitions/SharedElements/images/img1.jpg b/examples/NavigationPlayground/js/Transitions/SharedElements/images/img1.jpg
new file mode 100644
index 0000000000..c8d747db2d
Binary files /dev/null and b/examples/NavigationPlayground/js/Transitions/SharedElements/images/img1.jpg differ
diff --git a/examples/NavigationPlayground/js/Transitions/SharedElements/images/img10.jpg b/examples/NavigationPlayground/js/Transitions/SharedElements/images/img10.jpg
new file mode 100644
index 0000000000..cc1f224a6e
Binary files /dev/null and b/examples/NavigationPlayground/js/Transitions/SharedElements/images/img10.jpg differ
diff --git a/examples/NavigationPlayground/js/Transitions/SharedElements/images/img11.jpg b/examples/NavigationPlayground/js/Transitions/SharedElements/images/img11.jpg
new file mode 100644
index 0000000000..dbe61be8b9
Binary files /dev/null and b/examples/NavigationPlayground/js/Transitions/SharedElements/images/img11.jpg differ
diff --git a/examples/NavigationPlayground/js/Transitions/SharedElements/images/img12.jpg b/examples/NavigationPlayground/js/Transitions/SharedElements/images/img12.jpg
new file mode 100644
index 0000000000..92ef5ef5ce
Binary files /dev/null and b/examples/NavigationPlayground/js/Transitions/SharedElements/images/img12.jpg differ
diff --git a/examples/NavigationPlayground/js/Transitions/SharedElements/images/img2.jpg b/examples/NavigationPlayground/js/Transitions/SharedElements/images/img2.jpg
new file mode 100644
index 0000000000..096f10452f
Binary files /dev/null and b/examples/NavigationPlayground/js/Transitions/SharedElements/images/img2.jpg differ
diff --git a/examples/NavigationPlayground/js/Transitions/SharedElements/images/img3.jpg b/examples/NavigationPlayground/js/Transitions/SharedElements/images/img3.jpg
new file mode 100644
index 0000000000..919ba718cd
Binary files /dev/null and b/examples/NavigationPlayground/js/Transitions/SharedElements/images/img3.jpg differ
diff --git a/examples/NavigationPlayground/js/Transitions/SharedElements/images/img4.jpg b/examples/NavigationPlayground/js/Transitions/SharedElements/images/img4.jpg
new file mode 100644
index 0000000000..5523fade15
Binary files /dev/null and b/examples/NavigationPlayground/js/Transitions/SharedElements/images/img4.jpg differ
diff --git a/examples/NavigationPlayground/js/Transitions/SharedElements/images/img5.jpg b/examples/NavigationPlayground/js/Transitions/SharedElements/images/img5.jpg
new file mode 100644
index 0000000000..046697b462
Binary files /dev/null and b/examples/NavigationPlayground/js/Transitions/SharedElements/images/img5.jpg differ
diff --git a/examples/NavigationPlayground/js/Transitions/SharedElements/images/img6.jpg b/examples/NavigationPlayground/js/Transitions/SharedElements/images/img6.jpg
new file mode 100644
index 0000000000..124ffa60cc
Binary files /dev/null and b/examples/NavigationPlayground/js/Transitions/SharedElements/images/img6.jpg differ
diff --git a/examples/NavigationPlayground/js/Transitions/SharedElements/images/img7.jpg b/examples/NavigationPlayground/js/Transitions/SharedElements/images/img7.jpg
new file mode 100644
index 0000000000..e24973c29d
Binary files /dev/null and b/examples/NavigationPlayground/js/Transitions/SharedElements/images/img7.jpg differ
diff --git a/examples/NavigationPlayground/js/Transitions/SharedElements/images/img8.jpg b/examples/NavigationPlayground/js/Transitions/SharedElements/images/img8.jpg
new file mode 100644
index 0000000000..632dc1283a
Binary files /dev/null and b/examples/NavigationPlayground/js/Transitions/SharedElements/images/img8.jpg differ
diff --git a/examples/NavigationPlayground/js/Transitions/SharedElements/images/img9.jpg b/examples/NavigationPlayground/js/Transitions/SharedElements/images/img9.jpg
new file mode 100644
index 0000000000..1024a7109e
Binary files /dev/null and b/examples/NavigationPlayground/js/Transitions/SharedElements/images/img9.jpg differ
diff --git a/examples/NavigationPlayground/js/Transitions/SharedElements/index.js b/examples/NavigationPlayground/js/Transitions/SharedElements/index.js
new file mode 100644
index 0000000000..98beadf46c
--- /dev/null
+++ b/examples/NavigationPlayground/js/Transitions/SharedElements/index.js
@@ -0,0 +1,123 @@
+import {
+ StackNavigator,
+} from 'react-navigation';
+
+import PhotoGrid from './PhotoGrid';
+import PhotoDetail from './PhotoDetail';
+import { Transition } from 'react-navigation';
+import _ from 'lodash';
+import faker from 'faker';
+
+const {createTransition, initTransition, together, sq, Transitions} = Transition;
+
+const Slide = createTransition({
+ getStyleMap(
+ itemsOnFromRoute: Array<*>,
+ itemsOnToRoute: Array<*>,
+ transitionOptions,
+ ) {
+ // direction is 1 or -1
+ const { direction, layout: {initWidth} } = transitionOptions;
+ const routeToSlide = direction > 0 ? 'to' : 'from';
+ const itemsToSlide = direction > 0 ? itemsOnToRoute : itemsOnFromRoute;
+ const slide = (result, item) => {
+ result[item.id] = {
+ translateX: {
+ // inputRange: [0, 1] /// ===> [position, nextPosition]
+ outputRange: (direction > 0
+ ? [initWidth, 0]
+ : [0, initWidth]
+ )
+ }
+ }
+ return result;
+ }
+ return {
+ [routeToSlide]: itemsToSlide.reduce(slide, {}),
+ }
+ }
+});
+
+const SharedImage = initTransition(Transitions.SharedElement, /image-.+/);
+const CrossFadeScene = initTransition(Transitions.CrossFade, /\$scene.+/);
+const SlideScene = initTransition(Slide, /\$scene.+/);
+
+const StaggeredAppear = (filter) => ({
+ filter,
+ getStyleMap(
+ itemsOnFromRoute: Array<*>,
+ itemsOnToRoute: Array<*>,
+ transitionProps) {
+ const createStyle = (startTime, axis, direction) => {
+ const {progress} = transitionProps;
+ const inputRange = [0, startTime, 1];
+ const opacity = progress.interpolate({
+ inputRange,
+ outputRange: [0, 0, 1],
+ });
+ const translate = progress.interpolate({
+ inputRange,
+ outputRange: [ direction * 400, direction * 400, 0],
+ });
+ axis = axis === 'x' ? 'translateX' : 'translateY';
+ return {
+ opacity,
+ transform: [ { [axis]: translate } ],
+ };
+ }
+ const clamp = (x, min, max) => Math.max(min, Math.min(max, x));
+ const axes = ['x', 'y'];
+ const directions = [-1, 1];
+ return {
+ to: itemsOnToRoute.reduce((result, item) => {
+ const startTime = clamp(Math.random(), 0.1, 0.9);
+ const axis = faker.random.arrayElement(axes);
+ const direction = faker.random.arrayElement(directions);
+ result[item.id] = createStyle(startTime, axis, direction);
+ return result;
+ }, {}),
+ };
+ }
+})
+
+// const StaggeredAppearImages = createTransition(StaggeredAppear, /image-.+/);
+
+const NoOp = (filter) => ({
+ filter,
+ getStyleMap() {
+ console.log('NoOp transition called');
+ }
+});
+const NoOpImage = createTransition(NoOp, /image-.+/);
+
+const transitions = [
+ // { from: 'PhotoGrid', to: 'PhotoDetail', transition: CrossFadeScene(1) },
+ // { from: 'PhotoDetail', to: 'PhotoGrid', transition: CrossFadeScene(1) },
+ // { from: 'PhotoGrid', to: 'PhotoDetail', transition: NoOpImage},
+ // { from: 'PhotoDetail', to: 'PhotoGrid', transition: NoOpImage},
+ // { from: 'PhotoGrid', to: 'PhotoDetail', transition: together(SharedImage, DelayedFadeInDetail)},
+ // { from: 'PhotoDetail', to: 'PhotoGrid', transition: together(SharedImage, FastFadeOutDetail) },
+ // { from: 'PhotoGrid', to: 'PhotoDetail', transition: DelayedFadeInDetail},
+ // { from: 'PhotoDetail', to: 'PhotoGrid', transition: FastFadeOutDetail },
+ // { from: 'PhotoGrid', to: 'PhotoDetail', transition: SharedImage(1)},
+ // { from: 'PhotoDetail', to: 'PhotoGrid', transition: SharedImage(1)},
+ // { from: 'PhotoGrid', to: 'PhotoDetail', transition: CrossFadeScene },
+ // { from: 'PhotoDetail', to: 'PhotoGrid', transition: together(StaggeredAppearImages, SlideScenes) },
+ // { from: 'PhotoGrid', to: 'PhotoDetail', transition: together(SharedImage(), sq(Idle(0.9), CrossFadeScene()))},
+ // { from: 'PhotoDetail', to: 'PhotoGrid', transition: together(CrossFadeScene(0.1), SharedImage())},
+ { from: 'PhotoGrid', to: 'PhotoDetail', transition: sq(SharedImage(0.9), CrossFadeScene(0.1)), config: {duration: 650}},
+ { from: 'PhotoDetail', to: 'PhotoGrid', transition: sq(CrossFadeScene(0.1), SharedImage(0.9)), config: {duration: 650}},
+];
+
+const App = StackNavigator({
+ PhotoGrid: {
+ screen: PhotoGrid,
+ },
+ PhotoDetail: {
+ screen: PhotoDetail,
+ }
+}, {
+ transitions,
+ });
+
+export default App;
\ No newline at end of file
diff --git a/examples/NavigationPlayground/js/Transitions/index.js b/examples/NavigationPlayground/js/Transitions/index.js
new file mode 100644
index 0000000000..5540757c5b
--- /dev/null
+++ b/examples/NavigationPlayground/js/Transitions/index.js
@@ -0,0 +1,4 @@
+import SharedElements from './SharedElements';
+
+// TODO export a navigator
+export default SharedElements;
\ No newline at end of file
diff --git a/examples/NavigationPlayground/package.json b/examples/NavigationPlayground/package.json
index e76d22934e..10856e470d 100644
--- a/examples/NavigationPlayground/package.json
+++ b/examples/NavigationPlayground/package.json
@@ -5,6 +5,7 @@
"dependencies": {
"@exponent/vector-icons": "~4.0.0",
"exponent": "~13.1.0",
+ "faker": "^4.0.0",
"react": "~15.4.2",
"react-native": "github:exponent/react-native#sdk-13.0.0",
"react-native-vector-icons": "^4.0.0"
diff --git a/examples/NavigationPlayground/yarn.lock b/examples/NavigationPlayground/yarn.lock
index 98f78e88f4..6fcf2258fb 100644
--- a/examples/NavigationPlayground/yarn.lock
+++ b/examples/NavigationPlayground/yarn.lock
@@ -1,7 +1,5 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
-
-
"@exponent/vector-icons@~4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@exponent/vector-icons/-/vector-icons-4.0.0.tgz#772edabd5ad36472d29364729d62b928f4ca2d91"
@@ -428,7 +426,7 @@ babel-plugin-transform-es2015-computed-properties@^6.5.0, babel-plugin-transform
babel-runtime "^6.22.0"
babel-template "^6.22.0"
-babel-plugin-transform-es2015-destructuring@6.x, babel-plugin-transform-es2015-destructuring@^6.5.0, babel-plugin-transform-es2015-destructuring@^6.6.5, babel-plugin-transform-es2015-destructuring@^6.8.0:
+babel-plugin-transform-es2015-destructuring@^6.5.0, babel-plugin-transform-es2015-destructuring@^6.6.5, babel-plugin-transform-es2015-destructuring@^6.8.0, babel-plugin-transform-es2015-destructuring@6.x:
version "6.22.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.22.0.tgz#8e0af2f885a0b2cf999d47c4c1dd23ce88cfa4c6"
dependencies:
@@ -440,7 +438,7 @@ babel-plugin-transform-es2015-for-of@^6.5.0, babel-plugin-transform-es2015-for-o
dependencies:
babel-runtime "^6.22.0"
-babel-plugin-transform-es2015-function-name@6.x, babel-plugin-transform-es2015-function-name@^6.5.0, babel-plugin-transform-es2015-function-name@^6.8.0:
+babel-plugin-transform-es2015-function-name@^6.5.0, babel-plugin-transform-es2015-function-name@^6.8.0, babel-plugin-transform-es2015-function-name@6.x:
version "6.22.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.22.0.tgz#f5fcc8b09093f9a23c76ac3d9e392c3ec4b77104"
dependencies:
@@ -454,7 +452,7 @@ babel-plugin-transform-es2015-literals@^6.5.0, babel-plugin-transform-es2015-lit
dependencies:
babel-runtime "^6.22.0"
-babel-plugin-transform-es2015-modules-commonjs@6.x, babel-plugin-transform-es2015-modules-commonjs@^6.5.0, babel-plugin-transform-es2015-modules-commonjs@^6.7.0, babel-plugin-transform-es2015-modules-commonjs@^6.8.0:
+babel-plugin-transform-es2015-modules-commonjs@^6.5.0, babel-plugin-transform-es2015-modules-commonjs@^6.7.0, babel-plugin-transform-es2015-modules-commonjs@^6.8.0, babel-plugin-transform-es2015-modules-commonjs@6.x:
version "6.22.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.22.0.tgz#6ca04e22b8e214fb50169730657e7a07dc941145"
dependencies:
@@ -470,7 +468,7 @@ babel-plugin-transform-es2015-object-super@^6.6.5, babel-plugin-transform-es2015
babel-helper-replace-supers "^6.22.0"
babel-runtime "^6.22.0"
-babel-plugin-transform-es2015-parameters@6.x, babel-plugin-transform-es2015-parameters@^6.5.0, babel-plugin-transform-es2015-parameters@^6.7.0, babel-plugin-transform-es2015-parameters@^6.8.0:
+babel-plugin-transform-es2015-parameters@^6.5.0, babel-plugin-transform-es2015-parameters@^6.7.0, babel-plugin-transform-es2015-parameters@^6.8.0, babel-plugin-transform-es2015-parameters@6.x:
version "6.22.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.22.0.tgz#57076069232019094f27da8c68bb7162fe208dbb"
dependencies:
@@ -481,14 +479,14 @@ babel-plugin-transform-es2015-parameters@6.x, babel-plugin-transform-es2015-para
babel-traverse "^6.22.0"
babel-types "^6.22.0"
-babel-plugin-transform-es2015-shorthand-properties@6.x, babel-plugin-transform-es2015-shorthand-properties@^6.5.0, babel-plugin-transform-es2015-shorthand-properties@^6.8.0:
+babel-plugin-transform-es2015-shorthand-properties@^6.5.0, babel-plugin-transform-es2015-shorthand-properties@^6.8.0, babel-plugin-transform-es2015-shorthand-properties@6.x:
version "6.22.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.22.0.tgz#8ba776e0affaa60bff21e921403b8a652a2ff723"
dependencies:
babel-runtime "^6.22.0"
babel-types "^6.22.0"
-babel-plugin-transform-es2015-spread@6.x, babel-plugin-transform-es2015-spread@^6.5.0, babel-plugin-transform-es2015-spread@^6.6.5, babel-plugin-transform-es2015-spread@^6.8.0:
+babel-plugin-transform-es2015-spread@^6.5.0, babel-plugin-transform-es2015-spread@^6.6.5, babel-plugin-transform-es2015-spread@^6.8.0, babel-plugin-transform-es2015-spread@6.x:
version "6.22.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1"
dependencies:
@@ -626,7 +624,7 @@ babel-preset-es2015-node@^6.1.1:
babel-plugin-transform-es2015-unicode-regex "6.x"
semver "5.x"
-babel-preset-exponent@1.0.0:
+babel-preset-exponent@^1.0.0, babel-preset-exponent@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/babel-preset-exponent/-/babel-preset-exponent-1.0.0.tgz#07045ee1a6483952367c267f2acac74749257d7f"
dependencies:
@@ -803,14 +801,14 @@ balanced-match@^0.4.1:
version "0.4.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838"
-base64-js@0.0.8:
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-0.0.8.tgz#1101e9544f4a76b1bc3b26d452ca96d7a35e7978"
-
base64-js@^1.1.2:
version "1.2.0"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.0.tgz#a39992d723584811982be5e290bb6a53d86700f1"
+base64-js@0.0.8:
+ version "0.0.8"
+ resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-0.0.8.tgz#1101e9544f4a76b1bc3b26d452ca96d7a35e7978"
+
base64-url@1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/base64-url/-/base64-url-1.2.1.tgz#199fd661702a0e7b7dcae6e0698bb089c52f6d78"
@@ -1150,12 +1148,6 @@ dateformat@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-2.0.0.tgz#2743e3abb5c3fc2462e527dca445e04e9f4dee17"
-debug@2.3.3:
- version "2.3.3"
- resolved "https://registry.yarnpkg.com/debug/-/debug-2.3.3.tgz#40c453e67e6e13c901ddec317af8986cda9eff8c"
- dependencies:
- ms "0.7.2"
-
debug@^2.1.1, debug@^2.2.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.0.tgz#bc596bcabe7617f11d9fa15361eded5608b8499b"
@@ -1168,6 +1160,12 @@ debug@~2.2.0:
dependencies:
ms "0.7.1"
+debug@2.3.3:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.3.3.tgz#40c453e67e6e13c901ddec317af8986cda9eff8c"
+ dependencies:
+ ms "0.7.2"
+
decamelize@^1.0.0, decamelize@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
@@ -1247,14 +1245,14 @@ errorhandler@~1.4.2:
accepts "~1.3.0"
escape-html "~1.0.3"
-escape-html@1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.2.tgz#d77d32fa98e38c2f41ae85e9278e0e0e6ba1022c"
-
escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
+escape-html@1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.2.tgz#d77d32fa98e38c2f41ae85e9278e0e0e6ba1022c"
+
escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
@@ -1314,6 +1312,10 @@ extsprintf@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550"
+faker:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/faker/-/faker-4.0.0.tgz#fecb0a7a5fc950bc93377688498c67145dc135c8"
+
fancy-log@^1.1.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.3.0.tgz#45be17d02bb9917d60ccffd4995c999e6c8c9948"
@@ -1587,6 +1589,10 @@ http-signature@~1.1.0:
jsprim "^1.2.2"
sshpk "^1.7.0"
+iconv-lite@~0.4.13:
+ version "0.4.15"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb"
+
iconv-lite@0.4.11:
version "0.4.11"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.11.tgz#2ecb42fd294744922209a2e7c404dac8793d8ade"
@@ -1595,10 +1601,6 @@ iconv-lite@0.4.13:
version "0.4.13"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2"
-iconv-lite@~0.4.13:
- version "0.4.15"
- resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb"
-
image-size@^0.3.5:
version "0.3.5"
resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.3.5.tgz#83240eab2fb5b00b04aab8c74b0471e9cba7ad8c"
@@ -1618,7 +1620,7 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"
-inherits@2, inherits@~2.0.1:
+inherits@~2.0.1, inherits@2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
@@ -1701,14 +1703,14 @@ is-utf8@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
-isarray@0.0.1:
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
-
isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+isarray@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
+
isemail@1.x.x:
version "1.2.0"
resolved "https://registry.yarnpkg.com/isemail/-/isemail-1.2.0.tgz#be03df8cc3e29de4d2c5df6501263f1fa4595e9a"
@@ -2008,19 +2010,19 @@ mime-db@~1.23.0:
version "1.23.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.23.0.tgz#a31b4070adaea27d732ea333740a64d0ec9a6659"
-mime-types@2.1.11, mime-types@~2.1.7, mime-types@~2.1.9:
- version "2.1.11"
- resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.11.tgz#c259c471bda808a85d6cd193b430a5fae4473b3c"
- dependencies:
- mime-db "~1.23.0"
-
mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.13, mime-types@~2.1.6:
version "2.1.14"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.14.tgz#f7ef7d97583fcaf3b7d282b6f8b5679dab1e94ee"
dependencies:
mime-db "~1.26.0"
-mime@1.3.4, mime@^1.3.4:
+mime-types@~2.1.7, mime-types@~2.1.9, mime-types@2.1.11:
+ version "2.1.11"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.11.tgz#c259c471bda808a85d6cd193b430a5fae4473b3c"
+ dependencies:
+ mime-db "~1.23.0"
+
+mime@^1.3.4, mime@1.3.4:
version "1.3.4"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53"
@@ -2030,20 +2032,20 @@ min-document@^2.19.0:
dependencies:
dom-walk "^0.1.0"
-"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2:
+minimatch@^3.0.0, minimatch@^3.0.2, "minimatch@2 || 3":
version "3.0.3"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774"
dependencies:
brace-expansion "^1.0.0"
-minimist@0.0.8, minimist@~0.0.1:
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
-
minimist@^1.1.0, minimist@^1.1.1, minimist@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
+minimist@~0.0.1, minimist@0.0.8:
+ version "0.0.8"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
+
mkdirp@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
@@ -2259,7 +2261,7 @@ pinkie@^2.0.0:
version "2.0.4"
resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
-plist@1.2.0, plist@^1.2.0:
+plist@^1.2.0, plist@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/plist/-/plist-1.2.0.tgz#084b5093ddc92506e259f874b8d9b1afb8c79593"
dependencies:
@@ -2302,14 +2304,14 @@ punycode@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
-qs@4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/qs/-/qs-4.0.0.tgz#c31d9b74ec27df75e543a86c78728ed8d4623607"
-
qs@~6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.0.tgz#f403b264f23bc01228c74131b407f18d5ea5d442"
+qs@4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-4.0.0.tgz#c31d9b74ec27df75e543a86c78728ed8d4623607"
+
random-bytes@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b"
@@ -2345,7 +2347,7 @@ react-deep-force-update@^1.0.0:
color "^0.11.1"
lodash "^4.0.0"
-react-native-vector-icons@4.0.0, react-native-vector-icons@^4.0.0:
+react-native-vector-icons@^4.0.0, react-native-vector-icons@4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/react-native-vector-icons/-/react-native-vector-icons-4.0.0.tgz#6a2f8f4c6ace613aed7748cd530809283e5b1538"
dependencies:
@@ -2655,7 +2657,7 @@ sax@~1.1.1:
version "1.1.6"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.1.6.tgz#5d616be8a5e607d54e114afae55b7eaf2fcc3240"
-"semver@2 || 3 || 4 || 5", semver@5.x, semver@^5.0.3, semver@^5.1.0:
+semver@^5.0.3, semver@^5.1.0, "semver@2 || 3 || 4 || 5", semver@5.x:
version "5.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
@@ -2791,14 +2793,14 @@ stacktrace-parser@^0.1.3:
version "0.1.4"
resolved "https://registry.yarnpkg.com/stacktrace-parser/-/stacktrace-parser-0.1.4.tgz#01397922e5f62ecf30845522c95c4fe1d25e7d4e"
-statuses@1:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
-
statuses@~1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.2.1.tgz#dded45cc18256d51ed40aec142489d5c61026d28"
+statuses@1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
+
stream-buffers@~0.2.3:
version "0.2.6"
resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-0.2.6.tgz#181c08d5bb3690045f69401b9ae6a7a0cf3313fc"
@@ -2809,6 +2811,10 @@ stream-counter@~0.2.0:
dependencies:
readable-stream "~1.1.8"
+string_decoder@~0.10.x:
+ version "0.10.31"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
+
string-width@^1.0.1, string-width@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
@@ -2817,10 +2823,6 @@ string-width@^1.0.1, string-width@^1.0.2:
is-fullwidth-code-point "^1.0.0"
strip-ansi "^3.0.0"
-string_decoder@~0.10.x:
- version "0.10.31"
- resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
-
stringstream@~0.0.4:
version "0.0.5"
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
@@ -2852,6 +2854,10 @@ throat@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/throat/-/throat-3.0.0.tgz#e7c64c867cbb3845f10877642f7b60055b8ec0d6"
+through@^2.3.6:
+ version "2.3.8"
+ resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
+
through2@^2.0.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be"
@@ -2859,10 +2865,6 @@ through2@^2.0.0:
readable-stream "^2.1.5"
xtend "~4.0.1"
-through@^2.3.6:
- version "2.3.8"
- resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
-
time-stamp@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.0.1.tgz#9f4bd23559c9365966f3302dbba2b07c6b99b151"
@@ -2923,6 +2925,12 @@ uglify-to-browserify@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
+uid-safe@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.0.0.tgz#a7f3c6ca64a1f6a5d04ec0ef3e4c3d5367317137"
+ dependencies:
+ base64-url "1.2.1"
+
uid-safe@2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.3.tgz#077e264a00b3187936b270bb7376a26473631071"
@@ -2930,21 +2938,15 @@ uid-safe@2.1.3:
base64-url "1.3.3"
random-bytes "~1.0.0"
-uid-safe@~2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.0.0.tgz#a7f3c6ca64a1f6a5d04ec0ef3e4c3d5367317137"
- dependencies:
- base64-url "1.2.1"
-
ultron@1.0.x:
version "1.0.2"
resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa"
-unpipe@1.0.0, unpipe@~1.0.0:
+unpipe@~1.0.0, unpipe@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
-util-deprecate@1.0.2, util-deprecate@~1.0.1:
+util-deprecate@~1.0.1, util-deprecate@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
@@ -3003,14 +3005,14 @@ watch@~0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/watch/-/watch-0.10.0.tgz#77798b2da0f9910d595f1ace5b0c2258521f21dc"
-whatwg-fetch@>=0.10.0:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.2.tgz#fe294d1d89e36c5be8b3195057f2e4bc74fc980e"
-
whatwg-fetch@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-1.1.1.tgz#ac3c9d39f320c6dce5339969d054ef43dd333319"
+whatwg-fetch@>=0.10.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.2.tgz#fe294d1d89e36c5be8b3195057f2e4bc74fc980e"
+
which-module@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"
@@ -3025,14 +3027,14 @@ window-size@0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"
-wordwrap@0.0.2, wordwrap@~0.0.2:
- version "0.0.2"
- resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
-
wordwrap@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
+wordwrap@~0.0.2, wordwrap@0.0.2:
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
+
worker-farm@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.3.1.tgz#4333112bb49b17aa050b87895ca6b2cacf40e5ff"
@@ -3090,7 +3092,7 @@ xmldom@0.1.x:
version "0.1.27"
resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9"
-"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@~4.0.1:
+xtend@^4.0.0, "xtend@>=4.0.0 <4.1.0-0", xtend@~4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
@@ -3134,3 +3136,4 @@ yargs@~3.10.0:
cliui "^2.1.0"
decamelize "^1.0.0"
window-size "0.1.0"
+
diff --git a/src/navigators/StackNavigator.js b/src/navigators/StackNavigator.js
index 0b9b702788..0f3c1dc5bc 100644
--- a/src/navigators/StackNavigator.js
+++ b/src/navigators/StackNavigator.js
@@ -5,6 +5,7 @@ import createNavigationContainer from '../createNavigationContainer';
import createNavigator from './createNavigator';
import CardStack from '../views/CardStack';
import StackRouter from '../routers/StackRouter';
+import TransitionConfigs from '../views/TransitionConfigs';
import type {
NavigationContainerConfig,
@@ -31,6 +32,7 @@ export default (routeConfigMap: NavigationRouteConfigMap, stackConfig: StackNavi
onTransitionStart,
onTransitionEnd,
navigationOptions,
+ transitions,
} = stackConfig;
const stackRouterConfig = {
initialRouteName,
@@ -48,6 +50,7 @@ export default (routeConfigMap: NavigationRouteConfigMap, stackConfig: StackNavi
cardStyle={cardStyle}
onTransitionStart={onTransitionStart}
onTransitionEnd={onTransitionEnd}
+ transitionConfigs={transitions || TransitionConfigs.defaultTransitions()}
/>
)), containerOptions);
};
diff --git a/src/react-navigation.js b/src/react-navigation.js
index 014561205d..0cd1c5ba23 100644
--- a/src/react-navigation.js
+++ b/src/react-navigation.js
@@ -29,4 +29,6 @@ module.exports = {
// HOCs
get withNavigation() { return require('./views/withNavigation').default; },
+
+ get Transition() { return require('./views/Transition').default; },
};
diff --git a/src/views/Card.js b/src/views/Card.js
index b7da8e8c76..41be928846 100644
--- a/src/views/Card.js
+++ b/src/views/Card.js
@@ -11,6 +11,7 @@ import CardStackPanResponder from './CardStackPanResponder';
import CardStackStyleInterpolator from './CardStackStyleInterpolator';
import createPointerEventsContainer from './PointerEventsContainer';
import NavigationPropTypes from '../PropTypes';
+import Transition from './Transition';
import type {
NavigationPanHandlers,
@@ -27,6 +28,22 @@ type Props = NavigationSceneRendererProps & {
style: any,
};
+class TransitionStylesChange {
+ constructor() {
+ this._subscriptions = [];
+ }
+ subscribe(f) {
+ this._subscriptions.push(f);
+ }
+ unsubscribe(f) {
+ const idx = this._subscriptions.indexOf(f);
+ if (idx >= 0) this._subscriptions.splice(idx, 1);
+ }
+ dispatch(styleMap) {
+ this._subscriptions.forEach(f => f(styleMap));
+ }
+}
+
/**
* Component that renders the scene as card for the .
*/
@@ -43,6 +60,36 @@ class Card extends React.Component {
style: PropTypes.any,
};
+ static childContextTypes = {
+ // transitionProps: React.PropTypes.object.isRequired,
+ // transitionConfigs: React.PropTypes.array.isRequired,
+ routeName: React.PropTypes.string.isRequired,
+ transitionStylesChange: React.PropTypes.object,
+ };
+
+ props: Props;
+
+ constructor(props) {
+ super(props);
+ this._transitionStylesChange = new TransitionStylesChange();
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (this.props.transitionStyleMap !== nextProps.transitionStyleMap) {
+ this._transitionStylesChange.dispatch(nextProps.transitionStyleMap);
+ }
+ }
+
+ getChildContext() {
+ return {
+ // transitionProps: this.props.transitionProps,
+ // transitionConfigs: this.props.transitionConfigs,
+ routeName: this.props.scene.route.routeName,
+ transitionStylesChange: this._transitionStylesChange,
+ };
+ }
+
+
render() {
const {
panHandlers,
@@ -52,26 +99,24 @@ class Card extends React.Component {
...props /* NavigationSceneRendererProps */
} = this.props;
- const viewStyle = style === undefined ?
- CardStackStyleInterpolator.forHorizontal(props) :
- style;
-
const viewPanHandlers = panHandlers === undefined ?
CardStackPanResponder.forHorizontal({
...props,
onNavigateBack: this.props.onNavigateBack,
}) :
panHandlers;
-
+
return (
-
{renderScene(props)}
-
+
);
}
}
diff --git a/src/views/CardStack.js b/src/views/CardStack.js
index 68cea9b286..ac658aeba1 100644
--- a/src/views/CardStack.js
+++ b/src/views/CardStack.js
@@ -1,7 +1,9 @@
/* @flow */
import React, { PropTypes, Component } from 'react';
-import { Animated, StyleSheet, NativeModules, PanResponder, Platform, View, I18nManager, Keyboard } from 'react-native';
+import { Animated, StyleSheet, NativeModules, PanResponder, Platform, View, I18nManager, Keyboard, UIManager } from 'react-native';
+import invariant from 'invariant';
+import _ from 'lodash';
import Transitioner from './Transitioner';
import Card from './Card';
@@ -31,11 +33,18 @@ import type { TransitionConfig } from './TransitionConfigs';
import TransitionConfigs from './TransitionConfigs';
+import TransitionItems from './Transition/TransitionItems';
+import { convertStyleMap } from './Transition/transitionHelpers';
+
const emptyFunction = () => {};
const NativeAnimatedModule = NativeModules &&
NativeModules.NativeAnimatedModule;
+// The clone items delta must be bigger than the other value to avoid unwanted flickering.
+const OVERLAY_OPACITY_INPUT_RANGE_DELTA = 0.0001;
+const CLONE_ITEMS_OPACITY_INPUT_RANGE_DELTA = 0.01;
+
type Props = {
screenProps?: {},
headerMode: HeaderMode,
@@ -59,6 +68,9 @@ type DefaultProps = {
headerComponent: ReactClass<*>,
};
+type State = {
+ transitionItems: TransitionItems,
+};
/**
* The duration of the card animation in milliseconds.
@@ -91,13 +103,13 @@ const GESTURE_RESPONSE_DISTANCE = 35;
*/
const GESTURE_ANIMATED_VELOCITY_RATIO = -4;
-
class CardStack extends Component {
_render: NavigationSceneRenderer;
_renderScene: NavigationSceneRenderer;
_childNavigationProps: {
[key: string]: NavigationScreenProp<*, NavigationAction>,
} = {};
+ state: State;
/**
* Used to identify the starting point of the position when the gesture starts, such that it can
@@ -186,16 +198,81 @@ class CardStack extends Component {
style: View.propTypes.style,
};
+ static childContextTypes = {
+ registerTransitionItem: React.PropTypes.func,
+ unregisterTransitionItem: React.PropTypes.func,
+ }
+
static defaultProps: DefaultProps = {
mode: 'card',
headerComponent: Header,
};
+ constructor(props: Props, context) {
+ super(props, context);
+ this.state = {
+ transitionItems: new TransitionItems(),
+ }
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ if (this.props !== nextProps) {
+ return true;
+ } else {
+ return (
+ this.state.transitionItems !== nextState.transitionItems &&
+ nextState.transitionItems.areAllMeasured() &&
+ // prevent unnecesary updates when registering/unregistering transition items
+ this.state.transitionItems.count() === nextState.transitionItems.count()
+ );
+ }
+ }
+
+ componentWillReceiveProps(nextProps) {
+ // console.log('routeName', getRoute(this.props), 'nextRoute', getRoute(nextProps))
+
+ if (this.props.navigation !== nextProps.navigation) {
+ const getRoute = props => props.navigation && {
+ ...props.navigation.state.routes[props.navigation.state.index],
+ index: props.navigation.state.index,
+ };
+ const fromRoute = getRoute(this.props);
+ const toRoute = getRoute(nextProps);
+ this._fromRoute = fromRoute;
+ this._toRoute = toRoute;
+ this._receivedDifferentNavigationProp = true;
+ // When coming back from scene, onLayout won't be triggered, we'll need to do it manually.
+ this._setTransitionItemsState(prevItems => prevItems.removeAllMetrics(),
+ () => this._onLayout());
+ }
+ }
+
+ getChildContext() {
+ const self = this;
+ return {
+ registerTransitionItem(item: TransitionItem) {
+ // console.log('==> registering', item.toString());
+ self._setTransitionItemsState(prevItems => prevItems.add(item));
+ },
+ unregisterTransitionItem(id: string, routeName: string) {
+ // console.log('==> unregistering', id, routeName);
+ self._setTransitionItemsState(prevItems => prevItems.remove(id, routeName));
+ },
+ };
+ }
+
componentWillMount() {
this._render = this._render.bind(this);
this._renderScene = this._renderScene.bind(this);
}
+ // Just for testing //TODO delete when done with performance fixing
+ // componentDidUpdate(prevProps, prevState) {
+ // this.updateCount = this.updateCount || 0;
+ // this.updateCount++;
+ // console.log(`====================================> ${this.updateCount} cardStack updated propsChanged=${this.props !== prevProps}, stateChanged=${this.state !== prevState}, areAllMeasured=${this.state.transitionItems.areAllMeasured()}, prevState.areAllMeasured=${prevState.transitionItems.areAllMeasured()} sameItems?=${this.state.transitionItems===prevState.transitionItems}`);
+ // }
+
render() {
return (
{
...this._getTransitionConfig(
transitionProps,
prevTransitionProps
- ).transitionSpec,
+ ),
};
+ const transition = this._getTransition();
if (
!!NativeAnimatedModule &&
- // Native animation support also depends on the transforms used:
- CardStackStyleInterpolator.canUseNativeDriver(isModal)
+ // Native animation support also depends on the transforms used:
+ transition && transition.canUseNativeDriver()
) {
// Internal undocumented prop
transitionSpec.useNativeDriver = true;
@@ -283,6 +361,147 @@ class CardStack extends Component {
);
}
+ _hideTransitionViewUntilDone(transitionProps, onFromRoute: boolean) {
+ const {progress} = transitionProps;
+ const opacity = (onFromRoute
+ ? progress.interpolate({
+ inputRange: [0, CLONE_ITEMS_OPACITY_INPUT_RANGE_DELTA, 1],
+ outputRange: [1, 0, 0],
+ })
+ : progress.interpolate({
+ inputRange: [0, 1 - CLONE_ITEMS_OPACITY_INPUT_RANGE_DELTA, 1],
+ outputRange: [0, 0, 1],
+ })
+ );
+ return { opacity };
+ }
+
+ _replaceFromToInStyleMap(styleMap, fromRouteName: string, toRouteName: string) {
+ return {
+ [fromRouteName]: styleMap.from,
+ [toRouteName]: styleMap.to,
+ }
+ }
+
+ _findTransitionContainer() {
+ const fromRouteName = this._fromRoute && this._fromRoute.routeName;
+ const toRouteName = this._toRoute && this._toRoute.routeName;
+ const transitions = this.props.transitionConfigs.filter(c => (
+ (c.from === fromRouteName || c.from === '*') &&
+ (c.to === toRouteName || c.to === '*')));
+ invariant(transitions.length <= 1, `More than one transitions found from "${fromRouteName}" to "${toRouteName}".`);
+ return transitions[0];
+ }
+
+ _getTransition() {
+ const tc = this._findTransitionContainer();
+ return tc && tc.transition;
+ }
+
+ _getFilteredFromToItems(transition, fromRouteName: string, toRouteName: string) {
+ const isRoute = route => item => item.routeName === route;
+ const filterPass = item => transition && (!!!transition.filter || transition.filter(item.id));
+
+ const filteredItems = this.state.transitionItems.items().filter(filterPass);
+
+ const fromItems = filteredItems.filter(isRoute(fromRouteName));
+ const toItems = filteredItems.filter(isRoute(toRouteName));
+ return { from: fromItems, to: toItems };
+ }
+
+ _interpolateStyleMap(styleMap, transitionProps: NavigationTransitionProps) {
+ const interpolate = (value) => {
+ const delta = this._toRoute.index - this._fromRoute.index;
+ const { position } = transitionProps;
+ let { inputRange, outputRange } = value;
+ // Make sure the full [0, 1] inputRange is covered to avoid accidental output values
+ inputRange = [0, ...inputRange, 1].map(r => this._fromRoute.index + r * delta);
+ outputRange = [outputRange[0], ...outputRange, outputRange[outputRange.length - 1]];
+ if (delta < 0) {
+ inputRange = inputRange.reverse();
+ outputRange = outputRange.reverse();
+ }
+ return position.interpolate({
+ ...value,
+ inputRange,
+ outputRange,
+ });
+ };
+ return convertStyleMap(styleMap, interpolate, 'processTransform');
+ }
+
+ _createInPlaceTransitionStyleMap(
+ transitionProps: NavigationTransitionProps,
+ prevTransitionProps:NavigationTransitionProps) {
+ const fromRouteName = this._fromRoute && this._fromRoute.routeName;
+ const toRouteName = this._toRoute && this._toRoute.routeName;
+
+ const transition = this._getTransition();
+ if (!transition || !this.state.transitionItems.areAllMeasured()) {
+ return null;
+ }
+
+ const { from: fromItems, to: toItems } = this._getFilteredFromToItems(transition, fromRouteName, toRouteName);
+ const itemsToClone = transition.getItemsToClone && transition.getItemsToClone(fromItems, toItems);
+
+ const hideUntilDone = (items, onFromRoute: boolean) => items && items.reduce((result, item) => {
+ result[item.id] = this._hideTransitionViewUntilDone(transitionProps, onFromRoute);
+ return result;
+ }, {});
+
+ const styleMap = transition.getStyleMap &&
+ this._interpolateStyleMap(transition.getStyleMap(fromItems, toItems, transitionProps), transitionProps);
+ let inPlaceStyleMap = {
+ from: {
+ ...styleMap && styleMap.from,
+ ...hideUntilDone(itemsToClone, true), //TODO should we separate itemsToClone into from and to?
+ },
+ to: {
+ ...styleMap && styleMap.to,
+ ...hideUntilDone(itemsToClone, false),
+ }
+ };
+ inPlaceStyleMap = this._replaceFromToInStyleMap(inPlaceStyleMap, fromRouteName, toRouteName);
+
+ return inPlaceStyleMap;
+ }
+
+ _renderOverlay(transitionProps) {
+ const fromRouteName = this._fromRoute && this._fromRoute.routeName;
+ const toRouteName = this._toRoute && this._toRoute.routeName;
+ const transition = this._getTransition();
+ if (transition && this.state.transitionItems.areAllMeasured()) {
+ const { from: fromItems, to: toItems } = this._getFilteredFromToItems(transition, fromRouteName, toRouteName);
+ const itemsToClone = transition.getItemsToClone && transition.getItemsToClone(fromItems, toItems);
+ if (!itemsToClone) return null;
+
+ let styleMap = transition.getStyleMapForClones &&
+ this._interpolateStyleMap(transition.getStyleMapForClones(fromItems, toItems, transitionProps), transitionProps);
+ styleMap = styleMap && this._replaceFromToInStyleMap(styleMap, fromRouteName, toRouteName);
+
+ // TODO what if an item is the parent of another item?
+ const clones = itemsToClone.map(item => {
+ const animatedStyle = styleMap && styleMap[item.routeName] && styleMap[item.routeName][item.id];
+ return React.cloneElement(item.reactElement, {
+ style: [item.reactElement.props.style, styles.clonedItem, animatedStyle],
+ }, []);
+ });
+ const animatedContainerStyle = {
+ opacity: transitionProps.progress.interpolate({
+ inputRange: [0, OVERLAY_OPACITY_INPUT_RANGE_DELTA, 1 - OVERLAY_OPACITY_INPUT_RANGE_DELTA, 1],
+ outputRange: [0, 1, 1, 0],
+ })
+ };
+ return (
+
+ {clones}
+
+ );
+ } else {
+ return null;
+ }
+ }
+
_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
@@ -340,7 +559,9 @@ class CardStack extends Component {
});
}
- _render(props: NavigationTransitionProps): React.Element<*> {
+ _render(
+ props: NavigationTransitionProps,
+ prevTransitionProps:NavigationTransitionProps): React.Element<*> {
let floatingHeader = null;
const headerMode = this._getHeaderMode();
if (headerMode === 'float') {
@@ -448,6 +669,12 @@ class CardStack extends Component {
});
const gesturesEnabled = this.props.mode === 'card' && Platform.OS === 'ios';
const handlers = gesturesEnabled ? responder.panHandlers : {};
+
+ const styleMap = this._createInPlaceTransitionStyleMap(props, prevTransitionProps);
+ // console.log('==> inPlaceStyleMap', styleMap);
+
+ const overlay = this._renderOverlay(props);
+
return (
{
...props,
scene,
navigation: this._getChildNavigation(scene),
- }))}
+ }, prevTransitionProps, styleMap))}
{floatingHeader}
+ {overlay}
);
}
@@ -484,13 +712,14 @@ class CardStack extends Component {
transitionProps,
prevTransitionProps,
this.props.mode === 'modal'
- );
- if (this.props.transitionConfig) {
+ ).transitionSpec;
+ const tc = this._findTransitionContainer();
+ if (tc) {
return {
...defaultConfig,
- ...this.props.transitionConfig(),
- };
- }
+ ...tc.config,
+ }
+ }
return defaultConfig;
}
@@ -546,12 +775,94 @@ class CardStack extends Component {
return navigation;
};
- _renderScene(props: NavigationSceneRendererProps): React.Element<*> {
- const isModal = this.props.mode === 'modal';
+ _measure(item: TransitionItem): Promise < Metrics > {
+ return new Promise((resolve, reject) => {
+ UIManager.measureInWindow(
+ item.nativeHandle,
+ (x, y, width, height) => {
+ if ([x, y, width, height].every(n => _.isNumber(n))) {
+ resolve({ x, y, width, height });
+ } else {
+ reject(`x=${x}, y=${y}, width=${width}, height=${height}. The view (${item.toString()}) is not found. Is it collapsed on Android?`);
+ }
+ }
+ );
+ });
+ }
+
+ _setTransitionItemsState(fun, callback) {
+ this.setState(prevState => {
+ const newItems = fun(prevState.transitionItems);
+ return (newItems !== prevState.transitionItems
+ ? {...prevState, transitionItems: newItems}
+ : prevState
+ );
+ }, callback);
+ }
+
+ async _measureItems() {
+ const then = new Date();
+ const items = this.state.transitionItems.items().filter(i => i.shouldMeasure && !i.isMeasured());
+ let toUpdate = [];
+ for (let item of items) {
+ const { id, routeName } = item;
+ try {
+ const metrics = await this._measure(item);
+ toUpdate.push({ id, routeName, metrics });
+ // console.log('measured:', id, routeName, metrics);
+ } catch (err) {
+ console.warn(err);
+ }
+ }
+ if (toUpdate.length > 0) {
+ // console.log('measured, setting meatured state:', toUpdate)
+ this._setTransitionItemsState(prevItems => prevItems.updateMetrics(toUpdate));
+ }
+ console.log(`====> _measureItems took ${new Date() - then} ms`);
+ }
- /* $FlowFixMe */
- const { screenInterpolator } = this._getTransitionConfig();
- const style = screenInterpolator && screenInterpolator(props);
+ async _onLayout() {
+ // This guarantees that the measurement is only done after navigation.
+ // avoid unnecesary state updates when onLayout is called, e.g. when scrolling a ListView
+ if (!this._receivedDifferentNavigationProp) return;
+ this._receivedDifferentNavigationProp = false;
+
+ const fromRoute = this._fromRoute;
+ const toRoute = this._toRoute;
+ if (fromRoute && toRoute) {
+ const transition = this._getTransition();
+ let itemsToMeasure = [];
+ if (transition && transition.getItemsToMeasure) {
+ const { from, to } = this._getFilteredFromToItems(transition, fromRoute.routeName, toRoute.routeName);
+ itemsToMeasure = transition.getItemsToMeasure(from, to);
+ }
+ this._setTransitionItemsState(prevItems => prevItems.setShouldMeasure(itemsToMeasure),
+ () => this._measureItems());
+ }
+ }
+
+ /**
+ * By default, keep the current scene and not show the incoming scene (by setting their opacity)
+ * to prevent flickering and overdraw issues.
+ *
+ * @param {*} props
+ */
+ _createDefaultHideCardStyle(
+ props: NavigationSceneRendererProps,
+ prevTransitionProps: NavigationTransitionProps) {
+ const currentIndex = props.index;
+ const prevIndex = prevTransitionProps && prevTransitionProps.index;
+ const sceneIndex = props.scene.index;
+ const opacity = (prevIndex === null && currentIndex === sceneIndex) || prevIndex === sceneIndex ? 1 : 0;
+ // console.log('prevIndex', prevIndex, 'sceneIndex', sceneIndex, 'opacity', opacity);
+ return { opacity };
+ }
+
+ _renderScene(
+ props: NavigationSceneRendererProps,
+ prevTransitionProps: NavigationTransitionProps,
+ transitionStyleMap): React.Element<*> {
+ const isModal = this.props.mode === 'modal';
let panHandlers = null;
@@ -564,6 +875,8 @@ class CardStack extends Component {
props.scene.route.routeName
);
+ const defaultHideCardStyle = this._createDefaultHideCardStyle(props, prevTransitionProps);
+
return (
{
panHandlers={null}
renderScene={(sceneProps: *) =>
this._renderInnerCard(SceneComponent, sceneProps)}
- style={[style, this.props.cardStyle]}
+ style={[defaultHideCardStyle, this.props.cardStyle]}
+ onLayout={this._onLayout.bind(this)}
+ style={[defaultHideCardStyle, this.props.cardStyle]}
+ transitionStyleMap={transitionStyleMap}
/>
);
}
@@ -589,6 +905,17 @@ const styles = StyleSheet.create({
scenes: {
flex: 1,
},
+ overlay: {
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ top: 0,
+ bottom: 0,
+ elevation: 100, // make sure it's on the top on Android. TODO is this a legit way?
+ },
+ clonedItem: {
+ position: 'absolute',
+ }
});
export default CardStack;
diff --git a/src/views/Transition/TransitionItems.js b/src/views/Transition/TransitionItems.js
new file mode 100644
index 0000000000..2649ba1f7f
--- /dev/null
+++ b/src/views/Transition/TransitionItems.js
@@ -0,0 +1,132 @@
+// @flow
+import React from 'react';
+
+export type Metrics = {
+ x: number,
+ y: number,
+ width: number,
+ height: number,
+}
+
+export class TransitionItem {
+ id: string;
+ routeName: string;
+ reactElement: React.Element<*>;
+ nativeHandle: any;
+ metrics: ?Metrics;
+ shouldMeasure: boolean;
+ constructor(id: string, routeName: string, reactElement: React.Element<*>, nativeHandle: any, metrics:?Metrics, shouldMeasure: boolean = false) {
+ this.id = id;
+ this.routeName = routeName;
+ this.reactElement = reactElement;
+ this.nativeHandle = nativeHandle;
+ this.metrics = metrics;
+ this.shouldMeasure = shouldMeasure;
+ }
+ clone() {
+ return new TransitionItem(this.id, this.routeName, this.reactElement, this.nativeHandle, this.metrics, this.shouldMeasure);
+ }
+ toString() {
+ return `${this.id} ${this.routeName} handle=${this.nativeHandle} ${JSON.stringify(this.metrics)} shouldMeasure=${this.shouldMeasure}`;
+ }
+ isMeasured() {
+ const isNumber = n => typeof n === 'number';
+ const metricsValid = (m: Metrics) => m && [m.x, m.y, m.width, m.height].every(isNumber);
+ return metricsValid(this.metrics);
+ }
+}
+
+type ItemPair = {
+ fromItem: TransitionItem,
+ toItem: TransitionItem,
+};
+
+export type UpdateRequest = {
+ id: string,
+ routeName: string,
+ metrics: ?Metrics,
+}
+
+class TransitionItems {
+ _items: Array;
+ constructor(items: Array = []) {
+ this._items = [...items];
+ }
+ _findIndex(id: string, routeName: string): number {
+ return this._items.findIndex(i => {
+ return i.id === id && i.routeName === routeName;
+ });
+ }
+ count() {
+ return this._items.length;
+ }
+ items() {
+ return this._items;
+ }
+ add(item: TransitionItem): TransitionItems {
+ if (this._findIndex(item.id, item.routeName) >= 0)
+ return this;
+ else {
+ return new TransitionItems([...this._items, item]);
+ }
+ }
+ remove(id: string, routeName: string): TransitionItems {
+ const index = this._findIndex(id, routeName);
+ if (index >= 0) {
+ // if (id==='$scene-PhotoGrid') console.log('===> removing ', this._items[index].toString());
+ const newItems = [...this._items.slice(0, index), ...this._items.slice(index + 1)];
+ return new TransitionItems(newItems);
+ } else {
+ return this;
+ }
+ }
+ updateMetrics(requests: Array): TransitionItems {
+ const indexedRequests = requests.map(r => ({
+ ...r,
+ index: this._findIndex(r.id, r.routeName),
+ }));
+
+ if (indexedRequests.every(r => r.index < 0)) return this;
+ else {
+ let newItems = Array.from(this._items);
+ indexedRequests.forEach(r => {
+ if (r.index >= 0) {
+ const newItem = newItems[r.index].clone();
+ newItem.metrics = r.metrics;
+ newItems[r.index] = newItem;
+ }
+ });
+ return new TransitionItems(newItems);
+ }
+ }
+
+ removeAllMetrics(): TransitionItems {
+ if (this._items.some(i => !!i.metrics)) {
+ const newItems = this._items.map(item => {
+ const newItem = item.clone();
+ newItem.metrics = null;
+ return newItem;
+ });
+ return new TransitionItems(newItems);
+ } else return this;
+ }
+
+ areAllMeasured() {
+ return this._items.filter(i => i.shouldMeasure).every(i => i.isMeasured());
+ }
+
+ setShouldMeasure(itemsToMeasure: Array) {
+ if (itemsToMeasure && itemsToMeasure.length > 0) {
+ const newItems = this._items.map(item => {
+ const newItem = item.clone();
+ newItem.shouldMeasure = !!itemsToMeasure.find(i => i.id === item.id && i.routeName === item.routeName);
+ return newItem;
+ });
+ return new TransitionItems(newItems);
+ } else {
+ return this;
+ }
+ }
+}
+
+export default TransitionItems;
\ No newline at end of file
diff --git a/src/views/Transition/Transitions/CrossFade.js b/src/views/Transition/Transitions/CrossFade.js
new file mode 100644
index 0000000000..64c24dd82a
--- /dev/null
+++ b/src/views/Transition/Transitions/CrossFade.js
@@ -0,0 +1,21 @@
+// @flow
+
+import { createTransition } from '../transitionHelpers';
+
+const CrossFade = createTransition({
+ getStyleMap(
+ itemsOnFromRoute: Array<*>,
+ itemsOnToRoute: Array<*>,
+ ) {
+ const fade = (outputRange) => (result, item) => {
+ result[item.id] = { opacity: { outputRange } };
+ return result;
+ }
+ return {
+ from: itemsOnFromRoute.reduce(fade([1, 0]), {}),
+ to: itemsOnToRoute.reduce(fade([0, 1]), {}),
+ }
+ }
+});
+
+export default CrossFade;
\ No newline at end of file
diff --git a/src/views/Transition/Transitions/Scale.js b/src/views/Transition/Transitions/Scale.js
new file mode 100644
index 0000000000..14d25351df
--- /dev/null
+++ b/src/views/Transition/Transitions/Scale.js
@@ -0,0 +1,21 @@
+// @flow
+
+import { createTransition } from '../transitionHelpers';
+
+const Scale = createTransition({
+ getStyleMap(
+ itemsOnFromRoute: Array<*>,
+ itemsOnToRoute: Array<*>,
+ ) {
+ const scale = (outputRange) => (result, item) => {
+ result[item.id] = { scale: { outputRange } };
+ return result;
+ }
+ return {
+ from: itemsOnFromRoute.reduce(scale([1, 0]), {}),
+ to: itemsOnToRoute.reduce(scale([0, 1]), {}),
+ }
+ }
+});
+
+export default Scale;
\ No newline at end of file
diff --git a/src/views/Transition/Transitions/SharedElement.js b/src/views/Transition/Transitions/SharedElement.js
new file mode 100644
index 0000000000..41efefad6a
--- /dev/null
+++ b/src/views/Transition/Transitions/SharedElement.js
@@ -0,0 +1,57 @@
+// @flow
+
+import _ from 'lodash';
+import { createTransition } from '../transitionHelpers';
+
+const SharedElement = createTransition({
+ getItemsToClone(
+ itemsOnFromRoute: Array<*>,
+ itemsOnToRoute: Array<*> ) {
+ const itemIdsOnBoth = _.intersectionWith(itemsOnFromRoute, itemsOnToRoute, (i1, i2) => i1.id === i2.id)
+ .map(item => item.id);
+ const onBoth = item => itemIdsOnBoth.includes(item.id);
+ return itemsOnFromRoute.filter(onBoth);
+ },
+ getItemsToMeasure(
+ itemsOnFromRoute: Array<*>,
+ itemsOnToRoute: Array<*> ) {
+ const itemIdsOnBoth = _.intersectionWith(itemsOnFromRoute, itemsOnToRoute, (i1, i2) => i1.id === i2.id)
+ .map(item => item.id);
+ const onBoth = item => itemIdsOnBoth.includes(item.id);
+ return itemsOnFromRoute.filter(onBoth).concat(itemsOnToRoute.filter(onBoth));
+ },
+ getStyleMapForClones(
+ itemsOnFromRoute: Array<*>,
+ itemsOnToRoute: Array<*>,
+ transitionProps) {
+ const itemIdsOnBoth = _.intersectionWith(itemsOnFromRoute, itemsOnToRoute, (i1, i2) => i1.id === i2.id)
+ .map(item => item.id);
+ const createSharedItemStyle = (result, id) => {
+ const fromItem = itemsOnFromRoute.find(item => item.id === id);
+ const toItem = itemsOnToRoute.find(item => item.id === id);
+ console.log('fromItem', fromItem.toString(), 'toItem', toItem.toString());
+ const left = {
+ outputRange: [fromItem.metrics.x, toItem.metrics.x]
+ };
+ const top = {
+ outputRange: [fromItem.metrics.y, toItem.metrics.y]
+ };
+ const width = {
+ outputRange: [fromItem.metrics.width, toItem.metrics.width]
+ };
+ const height = {
+ outputRange: [fromItem.metrics.height, toItem.metrics.height]
+ };
+ result[id] = { left, top, width, height, right: null, bottom: null };
+ return result;
+ };
+ return {
+ from: itemIdsOnBoth.reduce(createSharedItemStyle, {}),
+ }
+ },
+ canUseNativeDriver() {
+ return false;
+ }
+});
+
+export default SharedElement;
\ No newline at end of file
diff --git a/src/views/Transition/Transitions/index.js b/src/views/Transition/Transitions/index.js
new file mode 100644
index 0000000000..c55417408d
--- /dev/null
+++ b/src/views/Transition/Transitions/index.js
@@ -0,0 +1,9 @@
+import SharedElement from './SharedElement';
+import CrossFade from './CrossFade';
+import Scale from './Scale';
+
+export default {
+ SharedElement,
+ CrossFade,
+ Scale,
+}
\ No newline at end of file
diff --git a/src/views/Transition/composition.js b/src/views/Transition/composition.js
new file mode 100644
index 0000000000..7231a36635
--- /dev/null
+++ b/src/views/Transition/composition.js
@@ -0,0 +1,123 @@
+import _ from 'lodash';
+import { convertStyleMap } from './transitionHelpers';
+
+function combineCommonProps(transitions) {
+ const filter = (id: string) => transitions.some(t => t.filter(id));
+ const getItemsOp = (op: string) => (
+ itemsOnFromRoute: Array<*>,
+ itemsOnToRoute: Array<*>) => transitions.reduce((result, t) => {
+ const fromItems = itemsOnFromRoute.filter(i => t.filter(i.id));
+ const toItems = itemsOnToRoute.filter(i => t.filter(i.id));
+ const opResult = t[op] && t[op](fromItems, toItems);
+ if (opResult) result = _.union(result, opResult);
+ return result;
+ }, []);
+ const canUseNativeDriver = () => (
+ transitions.every(t => _.isNil(t.canUseNativeDriver) || t.canUseNativeDriver())
+ );
+ const getItemsToClone = getItemsOp('getItemsToClone');
+ const getItemsToMeasure = getItemsOp('getItemsToMeasure');
+ return {
+ filter,
+ canUseNativeDriver,
+ getItemsToClone,
+ getItemsToMeasure,
+ };
+}
+
+const offsetStyleMap = (styleMap, start) => convertStyleMap(styleMap, (styleValue) => ({
+ ...styleValue,
+ inputRange: styleValue.inputRange.map(v => toFixed(start + v)),
+}));
+
+const mergeStyleValues = (styleValue1, styleValue2) => {
+ if ([styleValue1, styleValue2].some(_.isNil)) return styleValue1 || styleValue2;
+ if (styleValue1.outputRange && styleValue2.outputRange) {
+ //TODO [0, 0.2] + [0.3, 0.4] => [0, 0.2, 0.3, 0.4]
+ return styleValue2;
+ } else {
+ // otherwise, the value comes last overrides the values come before
+ return styleValue2;
+ }
+}
+
+const mergeStyle = (style1, style2) => {
+ if ([style1, style2].some(_.isNil)) return style1 || style2;
+ const props = _.union(Object.keys(style1), Object.keys(style2));
+ return props.reduce((result, prop) => {
+ result[prop] = mergeStyleValues(style1[prop], style2[prop]);
+ return result;
+ }, {});
+}
+
+const mergeStyles = (styles1, styles2) => {
+ if ([styles1, styles2].some(_.isNil)) return styles1 || styles2;
+ const ids = _.union(Object.keys(styles1), Object.keys(styles2));
+ return ids.reduce((result, id) => {
+ result[id] = mergeStyle(styles1[id], styles2[id]);
+ return result;
+ }, {});
+};
+
+const mergeStyleMap = (left, right) => ({
+ from: mergeStyles(left && left.from, right && right.from),
+ to: mergeStyles(left && left.to, right && right.to),
+});
+
+// http://stackoverflow.com/a/10474055/71133
+const toFixed = (n: number) => Math.round(n * 1e12) / 1e12;
+
+export function sequence(...transitions) {
+ const getStyleMapOp = (op: string) => (
+ itemsOnFromRoute: Array<*>,
+ itemsOnToRoute: Array<*>,
+ transitionProps) => {
+ const finalResult = transitions.reduce((result, t) => {
+ const fromItems = itemsOnFromRoute.filter(i => t.filter(i.id));
+ const toItems = itemsOnToRoute.filter(i => t.filter(i.id));
+ const styleMap = t[op] && t[op](fromItems, toItems, transitionProps);
+ const start = result.start || t.start || 0;
+ result = {
+ start: start + t.duration,
+ styleMap: mergeStyleMap(result.styleMap, offsetStyleMap(styleMap, start)),
+ };
+ return result;
+ }, {});
+ return finalResult.styleMap;
+ };
+ const duration = transitions.reduce((sum, t) => sum + t.duration, 0);
+ const getStyleMap = getStyleMapOp('getStyleMap');
+ const getStyleMapForClones = getStyleMapOp('getStyleMapForClones');
+ return {
+ ...combineCommonProps(transitions),
+ duration,
+ getStyleMap,
+ getStyleMapForClones,
+ };
+}
+
+export function together(...transitions) {
+ const getStyleMapOp = (op: string) => (
+ itemsOnFromRoute: Array<*>,
+ itemsOnToRoute: Array<*>,
+ transitionProps) => transitions.reduce((result, t) => {
+ // TODO duplicated code in sequence
+ const fromItems = itemsOnFromRoute.filter(i => t.filter(i.id));
+ const toItems = itemsOnToRoute.filter(i => t.filter(i.id));
+ const opResult = t[op] && t[op](fromItems, toItems, transitionProps);
+ if (opResult) result = mergeStyleMap(result, opResult);
+ return result;
+ }, {});
+ const duration = transitions.reduce((max, t) => Math.max(max, t.duration), 0);
+ const getStyleMap = getStyleMapOp('getStyleMap');
+ const getStyleMapForClones = getStyleMapOp('getStyleMapForClones');
+ return {
+ ...combineCommonProps(transitions),
+ duration,
+ getStyleMap,
+ getStyleMapForClones,
+ };
+}
+
+export const tg = together;
+export const sq = sequence;
\ No newline at end of file
diff --git a/src/views/Transition/createTransitionComponent.js b/src/views/Transition/createTransitionComponent.js
new file mode 100644
index 0000000000..846999f2f4
--- /dev/null
+++ b/src/views/Transition/createTransitionComponent.js
@@ -0,0 +1,161 @@
+// @flow
+
+import React from 'react';
+import {
+ View,
+ Image,
+ Text,
+ UIManager,
+ findNodeHandle,
+ Animated,
+} from 'react-native';
+
+import { TransitionItem } from './TransitionItems';
+import TransitionConfigs from '../TransitionConfigs';
+
+const statefulize = Component => {
+ class Statefulized extends React.Component {
+ // This is needed to avoid error from PointerEventsContainer
+ setNativeProps(props) {
+ this._component.setNativeProps(props);
+ }
+ render() {
+ return this._component = c} />;
+ }
+ }
+ return Statefulized;
+};
+
+const createAnimatedComponent = Component => {
+ if (Component === View) return Animated.View;
+ else if (Component === Image) return Animated.Image;
+ else if (Component === Text) return Animated.Text;
+ else {
+ // TODO: Perhaps need to cache the animated components created here by Component somewhere.
+ // otherwise, React will think it's a different type and refresh the component at every update.
+ // This might be the reason why doing below for all components such as View causes
+ // double-rendering of PhotoDetail (and causes a whole bunch of view not
+ // found errors when measuring views.
+ const isStatelessComponent = type => type.prototype && !!!type.prototype.render;
+ let C = Component;
+ if (isStatelessComponent(Component)) {
+ C = statefulize(Component);
+ }
+ // console.log('=====> createAnimatedComponent', Component.name || Component.displayName);
+ return Animated.createAnimatedComponent(C);
+ }
+};
+
+// function findTransitionConfig(transitionConfigs: Array<*>, routeName: string, prevRouteName: string) {
+// return transitionConfigs.find(c => c.from === prevRouteName && c.to === routeName);
+// }
+
+function createTransitionComponent(Component) {
+ class TransitionComponent extends React.Component {
+ _component: any;
+ static contextTypes = {
+ registerTransitionItem: React.PropTypes.func,
+ unregisterTransitionItem: React.PropTypes.func,
+ // transitionProps: React.PropTypes.object,
+ // transitionConfigs: React.PropTypes.array,
+ routeName: React.PropTypes.string,
+ transitionStylesChange: React.PropTypes.object,
+ };
+
+ constructor(props, context) {
+ super(props, context);
+ this._updateTransitionStyleMap = this._updateTransitionStyleMap.bind(this);
+ this.state = {
+ transitionStyleMap: null,
+ }
+ }
+
+ // This is needed to pass the invariant in PointerEventsContainer
+ setNativeProps(props) {
+ this._component.setNativeProps(props);
+ }
+
+ // _hideTransitionViewUntilDone(transitionProps) {
+ // const {position, scene: {index}} = transitionProps;
+ // const opacity = position.interpolate({
+ // inputRange: [index - 1, index - 0.01, index, index + 0.01, index + 1],
+ // outputRange: [0, 0, 1, 0, 0],
+ // });
+ // return { opacity };
+ // }
+
+ _getTransitionStyle() {
+ // const {id} = this.props;
+ // const {routeName, prevRouteName, transitionProps, transitionConfigs} = this.context;
+ // const transitionConfig = findTransitionConfig(transitionConfigs, routeName, prevRouteName);
+ // const transition = transitionConfig && transitionConfig.transition;
+ // const appliesToMe = transition && (!!!transition.filter || transition.filter(id));
+ // if (transition && appliesToMe) {
+ // return (transition.shouldClone && transition.shouldClone(id, routeName)
+ // ? this._hideTransitionViewUntilDone(transitionProps)
+ // : transition.createAnimatedStyle(id, routeName, transitionProps)
+ // );
+ // } else {
+ // // TODO this default should be set somewhere else
+ // return {};//TransitionConfigs.defaultTransitionConfig(transitionProps).screenInterpolator(transitionProps));
+ // }
+ const {id} = this.props;
+ const {routeName} = this.context;
+ const {transitionStyleMap} = this.state;
+ return transitionStyleMap && transitionStyleMap[routeName] && transitionStyleMap[routeName][id];
+ }
+
+ render() {
+ // collapsable={false} is required for UIManager.measureInWindow to get the actual measurements
+ // instead of undefined, see https://github.com/facebook/react-native/issues/9382
+ /*return (
+ this._component = c} style={{ flex: 1 }}>
+ {this._getAnimatedChild()}
+
+ )*/
+ const {id, ...rest} = this.props;
+ const AnimatedComponent = createAnimatedComponent(Component);
+ const animatedStyle = this._getTransitionStyle();
+ // console.log(`======> id=${id} styleInTransitionComp`, animatedStyle, 'props.style', this.props.style)
+ return (
+ this._component = c}
+ collapsable={false}
+ style={[this.props.style, animatedStyle]}
+ />
+ );
+ }
+
+ _updateTransitionStyleMap(transitionStyleMap) {
+ this.setState({transitionStyleMap});
+ }
+
+ componentDidMount() {
+ const { registerTransitionItem, transitionStylesChange } = this.context;
+
+ transitionStylesChange && transitionStylesChange.subscribe(this._updateTransitionStyleMap);
+
+ if (registerTransitionItem) {
+ const nativeHandle = findNodeHandle(this);
+ registerTransitionItem(new TransitionItem
+ (
+ this.props.id,
+ this.context.routeName,
+ this.render(),
+ nativeHandle,
+ ));
+ }
+ }
+
+ componentWillUnmount() {
+ const { unregisterTransitionItem, transitionStylesChange } = this.context;
+
+ transitionStylesChange && transitionStylesChange.unsubscribe(this._updateTransitionStyleMap);
+ unregisterTransitionItem && unregisterTransitionItem(this.props.id, this.context.routeName);
+ }
+ }
+ return TransitionComponent;
+}
+
+export default createTransitionComponent;
\ No newline at end of file
diff --git a/src/views/Transition/index.js b/src/views/Transition/index.js
new file mode 100644
index 0000000000..cf3013035b
--- /dev/null
+++ b/src/views/Transition/index.js
@@ -0,0 +1,24 @@
+import {
+ View,
+ Image,
+ Text,
+} from 'react-native';
+import TransitionItems from './TransitionItems';
+import createTransitionComponent from './createTransitionComponent';
+import { createTransition, initTransition } from './transitionHelpers';
+import { together, sequence, tg, sq } from './composition';
+import Transitions from './Transitions';
+
+export default {
+ View: createTransitionComponent(View),
+ Image: createTransitionComponent(Image),
+ Text: createTransitionComponent(Text),
+ createTransitionComponent,
+ createTransition,
+ initTransition,
+ together,
+ sequence,
+ tg,
+ sq,
+ Transitions,
+}
\ No newline at end of file
diff --git a/src/views/Transition/transitionHelpers.js b/src/views/Transition/transitionHelpers.js
new file mode 100644
index 0000000000..d2b2da9882
--- /dev/null
+++ b/src/views/Transition/transitionHelpers.js
@@ -0,0 +1,75 @@
+// @flow
+import invariant from 'invariant';
+import _ from 'lodash';
+
+function createIdRegexFilter(idRegexes) {
+ return (id: string) => idRegexes.every(idRegex => id.match(idRegex));
+}
+
+export function initTransition(Transition, ...idRegexes) {
+ return Transition && Transition(createIdRegexFilter(idRegexes));
+}
+
+const isTransformProp = (prop: string) => (
+ ['perspective', 'rotate', 'rotateX', 'rotateY', 'rotateZ',
+ 'scale', 'scaleX', 'scaleY', 'translateX', 'translateY', 'skewX', 'skewY'].includes(prop)
+);
+
+const mergeTransform = (transforms: ?Array<*>, prop: string, value: object) => {
+ const array = transforms || [];
+ const index = array.find(t => !_.isNil(t[prop]));
+ if (index >= 0) array[index][prop] = value;
+ else array.push({[prop]: value});
+ return array;
+}
+
+export function convertStyleMap(styleMap, convertStyleValue: (styleValue: object) => any, processTransform) {
+ const accumulateStyle = (result, styleValue, prop) => {
+ let convertedValue;
+ if (styleValue && _.isArray(styleValue.outputRange)) {
+ const inputRange = styleValue.inputRange || [0, 1];
+ convertedValue = convertStyleValue({ ...styleValue, inputRange });
+ } else {
+ convertedValue = styleValue;
+ }
+ if (processTransform && isTransformProp(prop)) {
+ result['transform'] = mergeTransform(result['transform'], prop, convertedValue);
+ } else {
+ result[prop] = convertedValue;
+ }
+ return result;
+ };
+ const accumulateStyles = (result, style, id) => {
+ result[id] = _.reduce(style, accumulateStyle, {});
+ return result;
+ };
+ return styleMap && _.reduce(styleMap, (result, styles, route) => {
+ result[route] = _.reduce(styles, accumulateStyles, {});
+ return result;
+ }, {});
+}
+
+const mashStyleMap = (styleMap, duration: number) => {
+ invariant(duration >= 0 && duration <= 1, 'duration must be in [0, 1]');
+ const mash = (styleValue) => ({
+ ...styleValue,
+ inputRange: styleValue.inputRange.map(v => v * duration),
+ });
+ return convertStyleMap(styleMap, mash);
+}
+
+export function createTransition(transitionConfig) {
+ const { getStyleMap, getStyleMapForClones, canUseNativeDriver, ...rest } = transitionConfig;
+ const getStyleMapHO = (op, duration) => (...args) => {
+ const originalStyleMap = transitionConfig[op] && transitionConfig[op](...args);
+ return mashStyleMap(originalStyleMap, duration);
+ };
+ return (filter) => (duration: number) => ({
+ filter,
+ duration,
+ canUseNativeDriver: canUseNativeDriver || (() => true),
+ getStyleMap: getStyleMapHO('getStyleMap', duration),
+ getStyleMapForClones: getStyleMapHO('getStyleMapForClones', duration),
+ ...rest,
+ });
+}
diff --git a/src/views/TransitionConfigs.js b/src/views/TransitionConfigs.js
index 7abfdf1f6e..627a9509c2 100644
--- a/src/views/TransitionConfigs.js
+++ b/src/views/TransitionConfigs.js
@@ -2,12 +2,13 @@
import type {
NavigationSceneRendererProps,
- NavigationTransitionProps,
- NavigationTransitionSpec,
+ NavigationTransitionProps,
+ NavigationTransitionSpec,
} from '../TypeDefinition';
import CardStackStyleInterpolator from './CardStackStyleInterpolator';
import HeaderStyleInterpolator from './HeaderStyleInterpolator';
+import Transition from './Transition';
import {
Animated,
@@ -93,7 +94,34 @@ function defaultTransitionConfig(
}
}
+const defaultTransition = (filter) => ({
+ filter,
+ getStyleMap(
+ itemsOnFromRoute: Array<*>,
+ itemsOnToRoute: Array<*>,
+ transitionProps) {
+ const createStyles = (items: Array<*>) => items.reduce((result, item) => {
+ const isModal = false; //TODO how to determine this here
+ const interpolator = defaultTransitionConfig(transitionProps, null, isModal).screenInterpolator;
+ result[item.id] = interpolator(transitionProps);
+ return result;
+ }, {});
+ return {
+ from: createStyles(itemsOnFromRoute),
+ to: createStyles(itemsOnToRoute),
+ };
+ }
+});
+
+// TODO Transition will resolve to undefined when the package is installed from npm.
+const DefaultSceneTransition = null;// Transition.createTransition(defaultTransition, /\$scene-.+/);
+
+const defaultTransitions = () => [
+ { from: '*', to: '*', transition: DefaultSceneTransition }
+];
+
export default {
DefaultTransitionSpec,
defaultTransitionConfig,
+ defaultTransitions,
};
diff --git a/src/views/__tests__/TransitionItems-test.js b/src/views/__tests__/TransitionItems-test.js
new file mode 100644
index 0000000000..f1cc863247
--- /dev/null
+++ b/src/views/__tests__/TransitionItems-test.js
@@ -0,0 +1,162 @@
+import TransitionItems, { TransitionItem } from '../Transition/TransitionItems';
+
+describe('TransitionItems', () => {
+ describe('add', () => {
+ it('returns new instance if not exists', () => {
+ const items = new TransitionItems();
+ const item = new TransitionItem();
+ const newItems = items.add(item);
+ expect(items).not.toBe(newItems);
+ })
+ it('returns same instance if already exists', () => {
+ const item = new TransitionItem('sharedView', 'route');
+ const items = new TransitionItems([item]);
+ const newItems = items.add(item);
+ expect(items).toBe(newItems);
+ })
+ it('adds many items', () => {
+ let items = new TransitionItems();
+ const n = 50;
+ for (let i = 0; i < n; i++) {
+ items = items.add(new TransitionItem(`item${i}`, 'route1'));
+ }
+ expect(items.count()).toBe(n);
+ })
+ })
+ describe('remove', () => {
+ it('returns new instance if found an item to remove', () => {
+ const item = new TransitionItem('sharedView', 'route');
+ const items = new TransitionItems([item]);
+ const newItems = items.remove(item.id, item.routeName);
+ expect(items).not.toBe(newItems);
+ });
+ it('returns same instance if item not found', () => {
+ const item = new TransitionItem('sharedView', 'route');
+ const items = new TransitionItems([item]);
+ const newItems = items.remove('sharedView2', 'route');
+ expect(items).toBe(newItems);
+ });
+ it('removes item properly', () => {
+ const item = new TransitionItem('sharedView', 'route');
+ const items = new TransitionItems([item]);
+ const newItems = items.remove(item.id, item.routeName);
+ expect(newItems._items.length).toBe(0);
+ })
+ })
+ describe('updateMetrics', () => {
+ it('returns new instance if found an item to update', () => {
+ const item = new TransitionItem('sharedView', 'route');
+ const items = new TransitionItems([item]);
+ const { id, routeName } = item;
+ const newItems = items.updateMetrics([{ id, routeName, metrics: { x: 1, y: 2 } }]);
+ expect(items).not.toBe(newItems);
+ });
+ it('returns same instance if item not found', () => {
+ const item = new TransitionItem('sharedView', 'route');
+ const items = new TransitionItems([item]);
+ const newItems = items.updateMetrics([{ id: 'blah', routeName: item.routeName, metrics: { x: 1, y: 2 } }]);
+ expect(items).toBe(newItems);
+ });
+ it('update item properly', () => {
+ const item = new TransitionItem('sharedView', 'route');
+ const items = new TransitionItems([item]);
+ const metrics = { x: 1, y: 2 };
+ const { id, routeName } = item;
+ const newItems = items.updateMetrics([{ id, routeName, metrics }]);
+ expect(newItems._items[0].metrics).toBe(metrics);
+ })
+ it('update multiple items', () => {
+ const item1 = new TransitionItem('sharedView', 'route');
+ const item2 = new TransitionItem('sharedView2', 'route');
+ const items = new TransitionItems([item1, item2]);
+ const metrics = { x: 1, y: 2 };
+ const requests = [
+ { id: item1.id, routeName: item1.routeName, metrics },
+ { id: item2.id, routeName: item2.routeName, metrics },
+ ]
+ const newItems = items.updateMetrics(requests);
+ expect(newItems._items[0].metrics).toBe(metrics);
+ expect(newItems._items[1].metrics).toBe(metrics);
+ })
+ it('does not update items not in requests', () => {
+ const item1 = new TransitionItem('sharedView', 'route');
+ const item2 = new TransitionItem('sharedView2', 'route');
+ const items = new TransitionItems([item1, item2]);
+ const metrics = { x: 1, y: 2 };
+ const requests = [
+ { id: item1.id, routeName: item1.routeName, metrics },
+ ]
+ const newItems = items.updateMetrics(requests);
+ expect(newItems._items.filter(i => i.metrics).length).toBe(1);
+ })
+ it('remove metrics', () => {
+ const metrics = { x: 1, y: 2 };
+ const item1 = new TransitionItem('sharedView', 'route', 'reactElement', 'nativeHandle', metrics);
+ const item2 = new TransitionItem('sharedView2', 'route', 'reactElement', 'nativeHandle', metrics);
+ const items = new TransitionItems([item1, item2]);
+ const requests = [
+ { id: item1.id, routeName: item1.routeName, metrics: null },
+ { id: item2.id, routeName: item2.routeName, metrics: null },
+ ]
+ const newItems = items.updateMetrics(requests);
+ expect(newItems._items[0].metrics).toBeNull();
+ expect(newItems._items[1].metrics).toBeNull();
+ })
+ })
+ describe('removeAllMetrics', () => {
+ it('returns new instance if some items has metrics', () => {
+ const metrics = { x: 1, y: 2 };
+ const item = new TransitionItem('sharedView', 'route', 'reactElement', 'nativeHandle', metrics);
+ const items = new TransitionItems([item]);
+ const newItems = items.removeAllMetrics();
+ expect(items).not.toBe(newItems);
+ });
+ it('returns same instance if none of items have metrics', () => {
+ const item = new TransitionItem('sharedView', 'route');
+ const items = new TransitionItems([item]);
+ const newItems = items.removeAllMetrics();
+ expect(items).toBe(newItems);
+ });
+ it('update item properly', () => {
+ const metrics = { x: 1, y: 2 };
+ const item = new TransitionItem('sharedView', 'route', 'reactElement', 'nativeHandle', metrics);
+ const items = new TransitionItems([item]);
+ const newItems = items.removeAllMetrics();
+ expect(newItems._items[0].metrics).toBeNull;
+ })
+ });
+ describe('areAllMeasured', () => {
+ it('returns true if all items contain metrics', () => {
+ const metrics = { x: 1, y: 2, width: 3, height: 4 };
+ const item1 = new TransitionItem('shared1', 'route1', 'reactElement', 'nativeHandle', metrics);
+ const item2 = new TransitionItem('shared1', 'route2', 'reactElement', 'nativeHandle', metrics);
+ const item3 = new TransitionItem('shared2', 'route1', 'reactElement', 'nativeHandle', metrics);
+ const item4 = new TransitionItem('shared2', 'route2', 'reactElement', 'nativeHandle', metrics);
+ const items = new TransitionItems([item1, item2, item3, item4]);
+ expect(items.areAllMeasured()).toBe(true);
+ });
+ it('returns false if some item should be measured but does not contain metrics', () => {
+ const metrics = { x: 1, y: 2, width: 3, height: 4 };
+ const item1 = new TransitionItem('shared1', 'route1', 'reactElement', 'nativeHandle', metrics);
+ const item2 = new TransitionItem('shared1', 'route2', 'reactElement', 'nativeHandle', null, true);
+ const item3 = new TransitionItem('shared2', 'route1', 'reactElement', 'nativeHandle', metrics);
+ const item4 = new TransitionItem('shared2', 'route2', 'reactElement', 'nativeHandle', metrics);
+ const items = new TransitionItems([item1, item2, item3, item4]);
+ expect(items.areAllMeasured()).toBe(false);
+ });
+ it('returns true if all "shouldMeasure" items contain metrics', () => {
+ const metrics = { x: 1, y: 2, width: 3, height: 4 };
+ const item1 = new TransitionItem('shared1', 'route1', 'reactElement', 'nativeHandle', metrics);
+ const item2 = new TransitionItem('shared1', 'route2', 'reactElement', 'nativeHandle', null, false);
+ const item3 = new TransitionItem('shared2', 'route3', 'reactElement', 'nativeHandle', metrics);
+ const item4 = new TransitionItem('shared2', 'route4', 'reactElement', 'nativeHandle', metrics);
+ const items = new TransitionItems([item1, item2, item3, item4]);
+ expect(items.areAllMeasured()).toBe(true);
+ });
+ it('returns true for empty items', () => {
+ const items = new TransitionItems();
+ expect(items.areAllMeasured()).toBe(true);
+ })
+ })
+})
+
diff --git a/src/views/__tests__/transitionComposition-test.js b/src/views/__tests__/transitionComposition-test.js
new file mode 100644
index 0000000000..52183e219c
--- /dev/null
+++ b/src/views/__tests__/transitionComposition-test.js
@@ -0,0 +1,227 @@
+// @flow
+
+import { together, sequence } from '../Transition/composition';
+import { createTransition, initTransition } from '../Transition/transitionHelpers';
+import { initTestTransition, assertIoRanges, ioRanges } from './transitionTestUtils';
+
+describe('together', () => {
+ it('accepts the union of ids that are accepted by all composed transitions', () => {
+ const dummy = createTransition({});
+ const t1 = initTransition(dummy, /foo/);
+ const t2 = initTransition(dummy, /bar/);
+ const composed = together(t1(), t2());
+ expect(composed.filter('foo')).toBe(true);
+ expect(composed.filter('bar')).toBe(true);
+ expect(composed.filter('blue2')).toBe(false);
+ });
+ it('only passes matching items to a child transition');
+ it('styleMap: [A(0.1), B(0.5)]', () => {
+ const a1 = 100, a2 = 200, b1 = 1000, b2 = 2000;
+ const A = initTestTransition('a', null, [a1, a2]);
+ const B = initTestTransition('b', null, [b1, b2]);
+ const combined = together(A(0.1), B(0.5));
+ const styleMap = combined.getStyleMap([], []);
+ const { from: {id1: {a, b}}} = styleMap;
+ assertIoRanges(a, {inputRange: [0, 0.1], outputRange: [a1, a2]});
+ assertIoRanges(b, {inputRange: [0, 0.5], outputRange: [b1, b2]});
+ });
+});
+
+describe('sequence', () => {
+ it('A(0.1) => B(0.2)', () => {
+ const a1 = 100, a2 = 200, b1 = 1000, b2 = 2000;
+ const A = initTestTransition('a', null, [a1, a2]);
+ const B = initTestTransition('b', null, [b1, b2]);
+ const combined = sequence(A(0.1), B(0.2));
+ const styleMap = combined.getStyleMap([], []);
+ const { from: {id1: {a, b}}} = styleMap;
+ assertIoRanges(a, {inputRange: [0, 0.1], outputRange: [a1, a2]});
+ assertIoRanges(b, {inputRange: [0.1, 0.3], outputRange: [b1, b2]});
+ });
+ it('A(0.1) => B(0.2) => C(0.4)', () => {
+ const a1 = 100, a2 = 200, b1 = 1000, b2 = 2000, c1 = 10000, c2 = 20000;
+ const A = initTestTransition('a', null, [a1, a2]);
+ const B = initTestTransition('b', null, [b1, b2]);
+ const C = initTestTransition('c', null, [c1, c2]);
+ const combined = sequence(A(0.1), B(0.2), C(0.3));
+ const styleMap = combined.getStyleMap([], []);
+ const { from: {id1: {a, b, c}}} = styleMap;
+ assertIoRanges(a, {inputRange: [0, 0.1], outputRange: [a1, a2]});
+ assertIoRanges(b, {inputRange: [0.1, 0.3], outputRange: [b1, b2]});
+ assertIoRanges(c, {inputRange: [0.3, 0.6], outputRange: [c1, c2]});
+ });
+ it('A(0.1) => (B(0.2) => C(0.4))', () => {
+ const a1 = 100, a2 = 200, b1 = 1000, b2 = 2000, c1 = 10000, c2 = 20000;
+ const A = initTestTransition('a', null, [a1, a2]);
+ const B = initTestTransition('b', null, [b1, b2]);
+ const C = initTestTransition('c', null, [c1, c2]);
+ const combined = sequence(A(0.1), sequence(B(0.2), C(0.3)));
+ const styleMap = combined.getStyleMap([], []);
+ const { from: {id1: {a, b, c}}} = styleMap;
+ assertIoRanges(a, {inputRange: [0, 0.1], outputRange: [a1, a2]});
+ assertIoRanges(b, {inputRange: [0.1, 0.3], outputRange: [b1, b2]});
+ assertIoRanges(c, {inputRange: [0.3, 0.6], outputRange: [c1, c2]});
+ });
+ it('A(0.1) => A2(0.2)');
+ it('A(0.1) => (B(0.2) => A2(0.4))');
+ it('A() => B()');
+ it('A(0.1) => B(0.2) => A2(0.4)');
+ it('A(0.5) => B(0.9)');
+ it('customized inputRange');
+});
+
+describe('Mixing together and sequence', () => {
+ it('A(0.1) => [B(0.2), C(0.4)]', () => {
+ const a1 = 100, a2 = 200, b1 = 1000, b2 = 2000, c1 = 10000, c2 = 20000;
+ const A = initTestTransition('a', null, [a1, a2]);
+ const B = initTestTransition('b', null, [b1, b2]);
+ const C = initTestTransition('c', null, [c1, c2]);
+ const combined = sequence(A(0.1), together(B(0.2), C(0.4)));
+ const styleMap = combined.getStyleMap([], []);
+ const { from: {id1: {a, b, c}}} = styleMap;
+ assertIoRanges(a, {inputRange: [0, 0.1], outputRange: [a1, a2]});
+ assertIoRanges(b, {inputRange: [0.1, 0.3], outputRange: [b1, b2]});
+ assertIoRanges(c, {inputRange: [0.1, 0.5], outputRange: [c1, c2]});
+ });
+ it('[A(0.1), B(0.2)] => C(0.4)', () => {
+ const a1 = 100, a2 = 200, b1 = 1000, b2 = 2000, c1 = 10000, c2 = 20000;
+ const A = initTestTransition('a', null, [a1, a2]);
+ const B = initTestTransition('b', null, [b1, b2]);
+ const C = initTestTransition('c', null, [c1, c2]);
+ const combined = sequence(together(A(0.1), B(0.2)), C(0.4));
+ const styleMap = combined.getStyleMap([], []);
+ const { from: {id1: {a, b, c}}} = styleMap;
+ assertIoRanges(a, {inputRange: [0, 0.1], outputRange: [a1, a2]});
+ assertIoRanges(b, {inputRange: [0, 0.2], outputRange: [b1, b2]});
+ assertIoRanges(c, {inputRange: [0.2, 0.6], outputRange: [c1, c2]});
+ });
+});
+
+/*
+
+A => { a: [a1, a2] }
+B => { b: [b1, b2] }
+A2 => { a: [a3, a4] }
+
+====== [A(), B()]
+A(): {
+ duration: 1,
+ style: {
+ a: { outputRange: [a1, a2] }
+ }
+}
+B(): {
+ duration: 1,
+ style: {
+ b: { outputRange: [b1, b2] }
+ }
+}
+[A(), B()]: {
+ duration: 1,
+ style: {
+ a: { outputRange: [a1, a2]},
+ b: { outputRange: [b1, b2]},
+ }
+}
+
+a = position.interpolate({
+ inputRange: [pos, pos + delta],
+ outputRange: [a1, a2]
+})
+b = position.interpolate({
+ inputRange: [pos, pos + delta],
+ outputRange: [b1, b2]
+})
+
+====== A(0.1) => B(0.2)
+A(0.1): {
+ duration: 0.1,
+ style:...
+}
+B(0.2): {
+ duration: 0.2,
+ style:...
+}
+A(0.1) => B(0.2): {
+ duration: [0.1, 0.2]
+}
+
+==
+
+{
+ from: {
+ id1: {
+ a: { inputRange: [0, 0.1], outputRange: [a1, a2]},
+ b: { inputRange: [0.1, 0.3], outputRange: [b1, b2]}
+ }
+ }
+}
+
+==
+
+a = position.interpolate({
+ inputRange: [pos, pos + 0.1 * delta],
+ outputRange: [a1, a2]
+})
+
+b = position.interpolate({
+ inputRange: [pos, pos + 0.1 * delta, pos + 0.3 * delta],
+ outputRange: [b1, b1, b2]
+})
+
+====== [A(), A2()]
+a = position.interpolate({
+ inputRange: [pos, pos + delta],
+ outputRange: [a3, a4]
+})
+
+====== A() => A2()
+a = position.interpolate({
+ inputRange: [pos, pos + 0.4999 * delta, pos + 0.5*delta, pos + delta],
+ outputRange: [a1, a2, a3, a4]
+})
+
+====== I(0.8) => A()
+I() => {}
+a = position.interpolate({
+ inputRange: [pos, pos + 0.8 * delta, pos + delta],
+ outputRange: [a1, a1, a2]
+})
+
+====== A1() => A2() => A3()
+A1() => { a: [a1, a2] }
+A2() => { a: [a2, a3] }
+A3() => { a: [a3, a4] }
+
+a = position.interpolate({
+ inputRange: [pos, pos + 0.33 * delta, pos + 0.66 * delta, pos + delta],
+ outputRange: [a1, a2, a3, a4]
+})
+
+====== [A(), 0.9 => B())]
+
+A() => { a: [a1, a2] }
+B() => { b: [b1, b2] }
+
+a = position.interpolate({
+ inputRange: [pos, pos + delta],
+ outputRange: [a1, a2],
+})
+
+b = position.interpolate({
+ inputRange: [pos, pos + 0.9 * delta, pos + delta],
+ outputRange: [b1, b1, b2]
+})
+
+====== [ AB(), 0.5 => B() ]
+AB() => { a: [a1, a2], b: [b1, b2]}
+B() => { b: [b3, b4] }
+
+a = ...
+b = position.interpolate({
+ inputRange: [pos, pos + delta* 0.4999, pos + delta * 0.5, pos + delta],
+ outputRange: [b1, b2, b3, b4]
+})
+
+// TODO: How to combine multiple easing functions?
+*/
\ No newline at end of file
diff --git a/src/views/__tests__/transitionHelpers-test.js b/src/views/__tests__/transitionHelpers-test.js
new file mode 100644
index 0000000000..80096ab618
--- /dev/null
+++ b/src/views/__tests__/transitionHelpers-test.js
@@ -0,0 +1,39 @@
+// @flow
+
+import { createTransition, initTransition, convertStyleMap } from '../Transition/transitionHelpers';
+import { initTestTransition, assertIoRanges, ioRanges } from './transitionTestUtils';
+
+describe('createTransition', () => {
+ it('returns styleMap based on duration: A(0.1)', () => {
+ const A = initTestTransition('a', [0, 1], [100, 200]);
+ const styleMap = A(0.1).getStyleMap();
+ const { from: { id1: { a } } } = styleMap;
+ assertIoRanges(a, ioRanges([0, 0.1], [100, 200]));
+ });
+});
+
+describe('convertStyleMap', () => {
+ it('properly handles transform', () => {
+ const styleMap = {
+ from: { id1: { scale: { outputRange: [0, 1] }, translateX: { outputRange: [10, 20] } } }
+ };
+ const identity = (styleValue) => styleValue;
+ const resultMap = convertStyleMap(styleMap, identity, 'processTransform');
+ expect(resultMap.from.id1).toEqual({
+ transform: [
+ { scale: { outputRange: [0, 1], inputRange: [0, 1] } },
+ { translateX: { outputRange: [10, 20], inputRange: [0, 1] } }]
+ });
+ });
+ it('does not process transforms when told not to do so', () => {
+ const styleMap = {
+ from: { id1: { scale: { outputRange: [0, 1] }, translateX: { outputRange: [10, 20] } } }
+ };
+ const identity = (styleValue) => styleValue;
+ const resultMap = convertStyleMap(styleMap, identity);
+ expect(resultMap.from.id1).toEqual({
+ scale: { outputRange: [0, 1], inputRange: [0, 1] },
+ translateX: { outputRange: [10, 20], inputRange: [0, 1] }
+ });
+ })
+})
\ No newline at end of file
diff --git a/src/views/__tests__/transitionTestUtils.js b/src/views/__tests__/transitionTestUtils.js
new file mode 100644
index 0000000000..e209bd70f0
--- /dev/null
+++ b/src/views/__tests__/transitionTestUtils.js
@@ -0,0 +1,25 @@
+// @flow
+import { createTransition, initTransition } from '../Transition/transitionHelpers';
+
+export function initTestTransition(prop, inputRange, outputRange, filterRegex = /foo/) {
+ const transition = createTransition({
+ getStyleMap() {
+ return {
+ from: {
+ id1: { [prop]: {inputRange, outputRange,} },
+ }
+ }
+ }
+ });
+ return initTransition(transition, filterRegex);
+}
+
+export function assertIoRanges(actual, expected) {
+ expect(actual.inputRange).toEqual(expected.inputRange);
+ expect(actual.outputRange).toEqual(expected.outputRange);
+}
+
+export function ioRanges(inputRange, outputRange) {
+ return {inputRange, outputRange};
+}
+