diff --git a/Libraries/NavigationExperimental/NavigationAnimatedView.js b/Libraries/NavigationExperimental/NavigationAnimatedView.js index 059407b206554a..5f307850640a42 100644 --- a/Libraries/NavigationExperimental/NavigationAnimatedView.js +++ b/Libraries/NavigationExperimental/NavigationAnimatedView.js @@ -11,6 +11,11 @@ */ 'use strict'; +/** + * WARNING: NavigationAnimatedView will be deprecated soon. + * Use NavigationTransitioner instead. + */ + const Animated = require('Animated'); const NavigationPropTypes = require('NavigationPropTypes'); const NavigationScenesReducer = require('NavigationScenesReducer'); @@ -40,6 +45,7 @@ type Props = { type State = { layout: NavigationLayout, position: NavigationAnimatedValue, + progress: NavigationAnimatedValue, scenes: Array, }; @@ -96,6 +102,9 @@ class NavigationAnimatedView this.state = { layout, position: new Animated.Value(this.props.navigationState.index), + // This `progress` is a adummy placeholder value to meet the values + // as `NavigationSceneRendererProps` requires. + progress: new Animated.Value(1), scenes: NavigationScenesReducer([], this.props.navigationState), }; } @@ -179,6 +188,7 @@ class NavigationAnimatedView const { position, + progress, scenes, } = this.state; @@ -187,6 +197,7 @@ class NavigationAnimatedView navigationState, onNavigate, position, + progress, scene, scenes, }); @@ -202,6 +213,7 @@ class NavigationAnimatedView const { position, + progress, scenes, } = this.state; @@ -210,6 +222,7 @@ class NavigationAnimatedView navigationState, onNavigate, position, + progress, scene: scenes[navigationState.index], scenes, }); diff --git a/Libraries/NavigationExperimental/NavigationExperimental.js b/Libraries/NavigationExperimental/NavigationExperimental.js index da03980cf7be88..ff8179189fd474 100644 --- a/Libraries/NavigationExperimental/NavigationExperimental.js +++ b/Libraries/NavigationExperimental/NavigationExperimental.js @@ -15,9 +15,10 @@ const NavigationAnimatedView = require('NavigationAnimatedView'); const NavigationCard = require('NavigationCard'); const NavigationCardStack = require('NavigationCardStack'); const NavigationHeader = require('NavigationHeader'); +const NavigationPropTypes = require('NavigationPropTypes'); const NavigationReducer = require('NavigationReducer'); const NavigationStateUtils = require('NavigationStateUtils'); -const NavigationPropTypes = require('NavigationPropTypes'); +const NavigationTransitioner = require('NavigationTransitioner'); const NavigationExperimental = { // Core @@ -26,6 +27,7 @@ const NavigationExperimental = { // Views AnimatedView: NavigationAnimatedView, + Transitioner: NavigationTransitioner, // CustomComponents: Card: NavigationCard, diff --git a/Libraries/NavigationExperimental/NavigationPropTypes.js b/Libraries/NavigationExperimental/NavigationPropTypes.js index 6939e8193465fc..bdbab9474c5fb0 100644 --- a/Libraries/NavigationExperimental/NavigationPropTypes.js +++ b/Libraries/NavigationExperimental/NavigationPropTypes.js @@ -70,6 +70,7 @@ const SceneRendererProps = { navigationState: navigationParentState.isRequired, onNavigate: PropTypes.func.isRequired, position: animatedValue.isRequired, + progress: animatedValue.isRequired, scene: scene.isRequired, scenes: PropTypes.arrayOf(scene).isRequired, }; @@ -103,6 +104,7 @@ function extractSceneRendererProps( navigationState: props.navigationState, onNavigate: props.onNavigate, position: props.position, + progress: props.progress, scene: props.scene, scenes: props.scenes, }; diff --git a/Libraries/NavigationExperimental/NavigationTransitioner.js b/Libraries/NavigationExperimental/NavigationTransitioner.js new file mode 100644 index 00000000000000..04776afa0be162 --- /dev/null +++ b/Libraries/NavigationExperimental/NavigationTransitioner.js @@ -0,0 +1,266 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule NavigationTransitioner + * @flow + */ +'use strict'; + +const Animated = require('Animated'); +const Easing = require('Easing'); +const NavigationPropTypes = require('NavigationPropTypes'); +const NavigationScenesReducer = require('NavigationScenesReducer'); +const React = require('React'); +const StyleSheet = require('StyleSheet'); +const View = require('View'); + +import type { + NavigationActionCaller, + NavigationAnimatedValue, + NavigationLayout, + NavigationParentState, + NavigationScene, + NavigationSceneRenderer, + NavigationTransitionConfigurator, +} from 'NavigationTypeDefinition'; + +type Props = { + configureTransition: NavigationTransitionConfigurator, + navigationState: NavigationParentState, + onNavigate: NavigationActionCaller, + onTransitionEnd: () => void, + onTransitionStart: () => void, + renderOverlay: ?NavigationSceneRenderer, + renderScene: NavigationSceneRenderer, + style: any, +}; + +type State = { + layout: NavigationLayout, + position: NavigationAnimatedValue, + progress: NavigationAnimatedValue, + scenes: Array, +}; + +const {PropTypes} = React; + +const DefaultTransitionSpec = { + duration: 250, + easing: Easing.inOut(Easing.ease), +}; + +function isSceneNotStale(scene: NavigationScene): boolean { + return !scene.isStale; +} + +class NavigationTransitioner extends React.Component { + + _onLayout: (event: any) => void; + _onTransitionEnd: () => void; + + props: Props; + state: State; + + static propTypes = { + configureTransition: PropTypes.func, + navigationState: NavigationPropTypes.navigationState.isRequired, + onNavigate: PropTypes.func.isRequired, + onTransitionEnd: PropTypes.func, + onTransitionStart: PropTypes.func, + renderOverlay: PropTypes.func, + renderScene: PropTypes.func.isRequired, + }; + + constructor(props: Props, context: any) { + super(props, context); + + // The initial layout isn't measured. Measured layout will be only available + // when the component is mounted. + const layout = { + height: new Animated.Value(0), + initHeight: 0, + initWidth: 0, + isMeasured: false, + width: new Animated.Value(0), + }; + + this.state = { + layout, + position: new Animated.Value(this.props.navigationState.index), + progress: new Animated.Value(1), + scenes: NavigationScenesReducer([], this.props.navigationState), + }; + } + + componentWillMount(): void { + this._onLayout = this._onLayout.bind(this); + this._onTransitionEnd = this._onTransitionEnd.bind(this); + } + + componentWillReceiveProps(nextProps: Props): void { + const nextScenes = NavigationScenesReducer( + this.state.scenes, + nextProps.navigationState, + this.props.navigationState + ); + + if (nextScenes === this.state.scenes) { + return; + } + + const { + position, + progress, + } = this.state; + + // update scenes. + this.setState({ + scenes: nextScenes, + }); + + // get the transition spec. + const transitionUserSpec = nextProps.configureTransition ? + nextProps.configureTransition() : + null; + + const transtionSpec = { + ...DefaultTransitionSpec, + ...transitionUserSpec, + }; + + progress.setValue(0); + + const animations = [ + Animated.timing( + progress, + { + ...transtionSpec, + toValue: 1, + }, + ), + ]; + + if (nextProps.navigationState.index !== this.props.navigationState.index) { + animations.push( + Animated.timing( + position, + { + ...transtionSpec, + toValue: nextProps.navigationState.index, + }, + ), + ); + } + + // play the transition. + nextProps.onTransitionStart && nextProps.onTransitionStart(); + Animated.parallel(animations).start(this._onTransitionEnd); + } + + render(): ReactElement { + const overlay = this._renderOverlay(); + const scenes = this._renderScenes(); + return ( + + + {scenes} + + {overlay} + + ); + } + + _renderScenes(): Array { + return this.state.scenes.map(this._renderScene, this); + } + + _renderScene(scene: NavigationScene): ?ReactElement { + const { + navigationState, + onNavigate, + renderScene, + } = this.props; + + const { + position, + progress, + scenes, + } = this.state; + + return renderScene({ + layout: this.state.layout, + navigationState, + onNavigate, + position, + progress, + scene, + scenes, + }); + } + + _renderOverlay(): ?ReactElement { + if (this.props.renderOverlay) { + const { + navigationState, + onNavigate, + renderOverlay, + } = this.props; + + const { + position, + progress, + scenes, + } = this.state; + + return renderOverlay({ + layout: this.state.layout, + navigationState, + onNavigate, + position, + progress, + scene: scenes[navigationState.index], + scenes, + }); + } + return null; + } + + _onLayout(event: any): void { + const {height, width} = event.nativeEvent.layout; + + const layout = { + ...this.state.layout, + initHeight: height, + initWidth: width, + isMeasured: true, + }; + + layout.height.setValue(height); + layout.width.setValue(width); + + this.setState({ layout }); + } + + _onTransitionEnd(): void { + const scenes = this.state.scenes.filter(isSceneNotStale); + if (scenes.length !== this.state.scenes.length) { + this.setState({ scenes }); + } + this.props.onTransitionEnd && this.props.onTransitionEnd(); + } +} + +const styles = StyleSheet.create({ + scenes: { + flex: 1, + }, +}); + +module.exports = NavigationTransitioner; diff --git a/Libraries/NavigationExperimental/NavigationTypeDefinition.js b/Libraries/NavigationExperimental/NavigationTypeDefinition.js index f0dff93ed9d5bd..8ee36194ac1418 100644 --- a/Libraries/NavigationExperimental/NavigationTypeDefinition.js +++ b/Libraries/NavigationExperimental/NavigationTypeDefinition.js @@ -41,8 +41,6 @@ export type NavigationLayout = { width: NavigationAnimatedValue, }; -export type NavigationPosition = NavigationAnimatedValue; - export type NavigationScene = { index: number, isStale: boolean, @@ -61,7 +59,14 @@ export type NavigationSceneRendererProps = { onNavigate: NavigationActionCaller, // The progressive index of the containing view's navigation state. - position: NavigationPosition, + position: NavigationAnimatedValue, + + // The value that represents the progress of the transition when navigation + // state changes from one to another. Its numberic value will range from 0 + // to 1. + // progress.__getAnimatedValue() < 1 : transtion is happening. + // progress.__getAnimatedValue() == 1 : transtion completes. + progress: NavigationAnimatedValue, // The scene to render. scene: NavigationScene, @@ -85,6 +90,12 @@ export type NavigationPanPanHandlers = { onStartShouldSetResponderCapture: Function, }; +export type NavigationTransitionSpec = { + duration?: number, + // An easing function from `Easing`. + easing?: () => any, +}; + // Functions. export type NavigationActionCaller = Function; @@ -112,3 +123,5 @@ export type NavigationSceneRenderer = ( export type NavigationStyleInterpolator = ( props: NavigationSceneRendererProps, ) => Object; + +export type NavigationTransitionConfigurator = () => NavigationTransitionSpec; diff --git a/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js b/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js index 9fe5781bb38697..450e28b5cb62b0 100644 --- a/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js +++ b/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js @@ -24,7 +24,7 @@ const SCENE_KEY_PREFIX = 'scene_'; * Helper function to compare route keys (e.g. "9", "11"). */ function compareKey(one: string, two: string): number { - var delta = one.length - two.length; + const delta = one.length - two.length; if (delta > 0) { return 1; } @@ -72,6 +72,9 @@ function NavigationScenesReducer( nextState: NavigationParentState, prevState: ?NavigationParentState, ): Array { + if (prevState === nextState) { + return scenes; + } const prevScenes = new Map(); const freshScenes = new Map();