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}; +} +