diff --git a/Examples/UIExplorer/NavigationExperimental/NavigationAnimatedExample.js b/Examples/UIExplorer/NavigationExperimental/NavigationAnimatedExample.js index b4bd9bc4352..3d97380702c 100644 --- a/Examples/UIExplorer/NavigationExperimental/NavigationAnimatedExample.js +++ b/Examples/UIExplorer/NavigationExperimental/NavigationAnimatedExample.js @@ -58,10 +58,11 @@ const NavigationBasicReducer = NavigationReducer.StackReducer({ class NavigationAnimatedExample extends React.Component { componentWillMount() { - this._renderNavigation = this._renderNavigation.bind(this); this._renderCard = this._renderCard.bind(this); - this._renderScene = this._renderScene.bind(this); this._renderHeader = this._renderHeader.bind(this); + this._renderNavigation = this._renderNavigation.bind(this); + this._renderScene = this._renderScene.bind(this); + this._renderTitleComponent = this._renderTitleComponent.bind(this); } render() { return ( @@ -99,14 +100,20 @@ class NavigationAnimatedExample extends React.Component { _renderHeader(/*NavigationSceneRendererProps*/ props) { return ( { - return {scene.navigationState.key}; - }} + {...props} + renderTitleComponent={this._renderTitleComponent} /> ); } + _renderTitleComponent(/*NavigationSceneRendererProps*/ props) { + return ( + + {props.scene.navigationState.key} + + ); + } + _renderCard(/*NavigationSceneRendererProps*/ props) { return ( { - return {stateTypeTitleMap(scene.navigationState)}; - }} + {...props} + renderTitleComponent={this._renderTitleComponent} /> ); } + _renderTitleComponent(props: NavigationSceneRendererProps) { + return ( + + {stateTypeTitleMap(props.scene.navigationState)} + + ); + } + _renderScene(props: NavigationSceneRendererProps) { const {onNavigate} = props; return ( diff --git a/Examples/UIExplorer/UIExplorerApp.ios.js b/Examples/UIExplorer/UIExplorerApp.ios.js index d098a99cbd6..caccd255ebc 100644 --- a/Examples/UIExplorer/UIExplorerApp.ios.js +++ b/Examples/UIExplorer/UIExplorerApp.ios.js @@ -1,4 +1,11 @@ /** + * Copyright (c) 2013-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. + * * The examples provided by Facebook are for non-commercial testing and * evaluation purposes only. * @@ -77,10 +84,12 @@ class UIExplorerApp extends React.Component { _renderOverlay: Function; _renderScene: Function; _renderCard: Function; + _renderTitleComponent: Function; componentWillMount() { this._renderNavigation = this._renderNavigation.bind(this); this._renderOverlay = this._renderOverlay.bind(this); this._renderScene = this._renderScene.bind(this); + this._renderTitleComponent = this._renderTitleComponent.bind(this); } render() { return ( @@ -121,15 +130,20 @@ class UIExplorerApp extends React.Component { _renderOverlay(props: NavigationSceneRendererProps): ReactElement { return ( { - return {UIExplorerStateTitleMap(scene.navigationState)}; - }} + {...props} + renderTitleComponent={this._renderTitleComponent} /> ); } + _renderTitleComponent(props: NavigationSceneRendererProps): ReactElement { + return ( + + {UIExplorerStateTitleMap(props.scene.navigationState)} + + ); + } + _renderScene(props: NavigationSceneRendererProps): ?ReactElement { const state = props.scene.navigationState; if (state.key === 'AppList') { diff --git a/Libraries/CustomComponents/NavigationExperimental/NavigationHeader.js b/Libraries/CustomComponents/NavigationExperimental/NavigationHeader.js index 1dd497b4344..403ec0ad171 100644 --- a/Libraries/CustomComponents/NavigationExperimental/NavigationHeader.js +++ b/Libraries/CustomComponents/NavigationExperimental/NavigationHeader.js @@ -1,5 +1,10 @@ /** - * Copyright (c) 2015, Facebook, Inc. All rights reserved. + * Copyright (c) 2013-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. * * Facebook, Inc. ("Facebook") owns all right, title and interest, including * all intellectual property and other proprietary rights, in and to the React @@ -32,6 +37,8 @@ const NavigationContainer = require('NavigationContainer'); const NavigationHeaderTitle = require('NavigationHeaderTitle'); const NavigationHeaderBackButton = require('NavigationHeaderBackButton'); const NavigationPropTypes = require('NavigationPropTypes'); +const NavigationHeaderStyleInterpolator = require('NavigationHeaderStyleInterpolator'); +const ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin'); const { Animated, @@ -41,210 +48,197 @@ const { } = React; import type { + NavigationSceneRenderer, NavigationSceneRendererProps, - NavigationScene, + NavigationStyleInterpolator, } from 'NavigationTypeDefinition'; -type Renderer = (props: NavigationSceneRendererProps, scene: NavigationScene) => ?ReactElement; - type DefaultProps = { - renderTitleComponent: Renderer; - renderLeftComponent: Renderer; + renderLeftComponent: NavigationSceneRenderer, + renderRightComponent: NavigationSceneRenderer, + renderTitleComponent: NavigationSceneRenderer, }; -type Props = { - navigationProps: NavigationSceneRendererProps; - renderTitleComponent: Renderer; - renderLeftComponent: Renderer; - renderRightComponent: Renderer; +type Props = NavigationSceneRendererProps & { + renderLeftComponent: NavigationSceneRenderer, + renderRightComponent: NavigationSceneRenderer, + renderTitleComponent: NavigationSceneRenderer, style?: any; -} +}; + +type SubViewName = 'left' | 'title' | 'right'; const APPBAR_HEIGHT = Platform.OS === 'ios' ? 44 : 56; const STATUSBAR_HEIGHT = Platform.OS === 'ios' ? 20 : 0; +const {PropTypes} = React; + +class NavigationHeader extends React.Component { + props: Props; -class NavigationHeader extends React.Component { static defaultProps = { - renderTitleComponent: (props, scene) => { - const pageState = scene.navigationState; - return {pageState.title ? pageState.title : ''}; + renderTitleComponent: (props: NavigationSceneRendererProps) => { + const {navigationState} = props; + const title = String(navigationState.title || ''); + return {title}; + }, + + renderLeftComponent: (props: NavigationSceneRendererProps) => { + return props.scene.index > 0 ? : null; + }, + + renderRightComponent: (props: NavigationSceneRendererProps) => { + return null; }, - renderLeftComponent: (props, scene) => scene.index !== 0 ? : null }; static propTypes = { - navigationProps: React.PropTypes.shape(NavigationPropTypes.SceneRenderer).isRequired, - renderTitleComponent: React.PropTypes.func, - renderLeftComponent: React.PropTypes.func, - renderRightComponent: React.PropTypes.func, + ...NavigationPropTypes.SceneRenderer, + renderLeftComponent: PropTypes.func, + renderRightComponent: PropTypes.func, + renderTitleComponent: PropTypes.func, style: View.propTypes.style, }; - _renderLeftComponent(scene: NavigationScene) { - const { - renderLeftComponent, - navigationProps, - } = this.props; - - if (renderLeftComponent) { - const { - index, - navigationState, - } = scene; - - return ( - - {renderLeftComponent(navigationProps, scene)} - - ); - } - - return null; + shouldComponentUpdate(nextProps: Props, nextState: any): boolean { + return ReactComponentWithPureRenderMixin.shouldComponentUpdate.call( + this, + nextProps, + nextState + ); } - _renderRightComponent(scene: NavigationScene) { - const { - renderRightComponent, - navigationProps, - } = this.props; + render(): ReactElement { + const { scenes, style } = this.props; - if (renderRightComponent) { - const { - index, - navigationState, - } = scene; - - return ( - - {renderRightComponent(navigationProps, scene)} - - ); - } - - return null; - } - - _renderTitleComponent(scene: NavigationScene) { - const { - renderTitleComponent, - navigationProps, - } = this.props; - - if (renderTitleComponent) { - const { - index, - navigationState, - } = scene; - - return ( - - {renderTitleComponent(navigationProps, scene)} - - ); - } - - return null; - } - - render() { - const { scenes } = this.props.navigationProps; + const scenesProps = scenes.map(scene => { + const props = NavigationPropTypes.extractSceneRendererProps(this.props); + props.scene = scene; + return props; + }); return ( - - {scenes.map(this._renderLeftComponent, this)} - {scenes.map(this._renderTitleComponent, this)} - {scenes.map(this._renderRightComponent, this)} + + {scenesProps.map(this._renderLeft, this)} + {scenesProps.map(this._renderTitle, this)} + {scenesProps.map(this._renderRight, this)} ); } + + _renderLeft(props: NavigationSceneRendererProps): ?ReactElement { + return this._renderSubView( + props, + 'left', + this.props.renderLeftComponent, + NavigationHeaderStyleInterpolator.forLeft, + ); + } + + _renderTitle(props: NavigationSceneRendererProps): ?ReactElement { + return this._renderSubView( + props, + 'title', + this.props.renderTitleComponent, + NavigationHeaderStyleInterpolator.forCenter, + ); + } + + _renderRight(props: NavigationSceneRendererProps): ?ReactElement { + return this._renderSubView( + props, + 'right', + this.props.renderRightComponent, + NavigationHeaderStyleInterpolator.forRight, + ); + } + + _renderSubView( + props: NavigationSceneRendererProps, + name: SubViewName, + renderer: NavigationSceneRenderer, + styleInterpolator: NavigationStyleInterpolator, + ): ?ReactElement { + const { + scene, + navigationState, + } = props; + + const { + index, + isStale, + key, + } = scene; + + const offset = navigationState.index - index; + + if (Math.abs(offset) > 2) { + // Scene is far away from the active scene. Hides it to avoid unnecessary + // rendering. + return null; + } + + const subView = renderer(props); + if (subView === null) { + return null; + } + + const pointerEvents = offset !== 0 || isStale ? 'none' : 'auto'; + return ( + + {subView} + + ); + } } const styles = StyleSheet.create({ appbar: { - position: 'absolute', - top: 0, - left: 0, - right: 0, - flexDirection: 'row', alignItems: 'center', - justifyContent: 'flex-start', backgroundColor: Platform.OS === 'ios' ? '#EFEFF2' : '#FFF', - borderBottomWidth: Platform.OS === 'ios' ? StyleSheet.hairlineWidth : 0, borderBottomColor: 'rgba(0, 0, 0, .15)', - height: APPBAR_HEIGHT + STATUSBAR_HEIGHT, - marginBottom: 16, // This is needed for elevation shadow + borderBottomWidth: Platform.OS === 'ios' ? StyleSheet.hairlineWidth : 0, elevation: 2, + flexDirection: 'row', + height: APPBAR_HEIGHT + STATUSBAR_HEIGHT, + justifyContent: 'flex-start', + left: 0, + marginBottom: 16, // This is needed for elevation shadow + position: 'absolute', + right: 0, + top: 0, }, title: { - position: 'absolute', - top: 0, bottom: 0, left: APPBAR_HEIGHT, - right: APPBAR_HEIGHT, marginTop: STATUSBAR_HEIGHT, + position: 'absolute', + right: APPBAR_HEIGHT, + top: 0, }, left: { - position: 'absolute', - top: 0, bottom: 0, left: 0, marginTop: STATUSBAR_HEIGHT, + position: 'absolute', + top: 0, }, right: { - position: 'absolute', - top: 0, bottom: 0, - right: 0, marginTop: STATUSBAR_HEIGHT, - } + position: 'absolute', + right: 0, + top: 0, + }, }); NavigationHeader = NavigationContainer.create(NavigationHeader); diff --git a/Libraries/CustomComponents/NavigationExperimental/NavigationHeaderStyleInterpolator.js b/Libraries/CustomComponents/NavigationExperimental/NavigationHeaderStyleInterpolator.js new file mode 100644 index 00000000000..f6a0dbf5166 --- /dev/null +++ b/Libraries/CustomComponents/NavigationExperimental/NavigationHeaderStyleInterpolator.js @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2013-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. + * + * Facebook, Inc. ("Facebook") owns all right, title and interest, including + * all intellectual property and other proprietary rights, in and to the React + * Native CustomComponents software (the "Software"). Subject to your + * compliance with these terms, you are hereby granted a non-exclusive, + * worldwide, royalty-free copyright license to (1) use and copy the Software; + * and (2) reproduce and distribute the Software as part of your own software + * ("Your Software"). Facebook reserves all rights not expressly granted to + * you in this license agreement. + * + * THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED. + * IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR + * EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @providesModule NavigationHeaderStyleInterpolator + * @flow + */ +'use strict'; + + +import type { + NavigationSceneRendererProps, +} from 'NavigationTypeDefinition'; + +/** + * Utility that builds the style for the navigation header. + * + * +-------------+-------------+-------------+ + * | | | | + * | Left | Title | Right | + * | Component | Component | Component | + * | | | | + * +-------------+-------------+-------------+ + */ + +function forLeft(props: NavigationSceneRendererProps): Object { + const {position, scene} = props; + const {index} = scene; + return { + opacity: position.interpolate({ + inputRange: [ index - 1, index, index + 1 ], + outputRange: [ 0, 1, 0 ], + }), + }; +} + +function forCenter(props: NavigationSceneRendererProps): Object { + const {position, scene} = props; + const {index} = scene; + return { + opacity:position.interpolate({ + inputRange: [ index - 1, index, index + 1 ], + outputRange: [ 0, 1, 0 ], + }), + transform: [ + { + translateX: position.interpolate({ + inputRange: [ index - 1, index + 1 ], + outputRange: [ 200, -200 ], + }), + } + ], + }; +} + +function forRight(props: NavigationSceneRendererProps): Object { + const {position, scene} = props; + const {index} = scene; + return { + opacity: position.interpolate({ + inputRange: [ index - 1, index, index + 1 ], + outputRange: [ 0, 1, 0 ], + }), + }; +} + +module.exports = { + forCenter, + forLeft, + forRight, +}; diff --git a/Libraries/NavigationExperimental/NavigationPropTypes.js b/Libraries/NavigationExperimental/NavigationPropTypes.js index 841895f5101..ef163d8a17f 100644 --- a/Libraries/NavigationExperimental/NavigationPropTypes.js +++ b/Libraries/NavigationExperimental/NavigationPropTypes.js @@ -11,6 +11,10 @@ */ 'use strict'; +import type { + NavigationSceneRendererProps, +} from 'NavigationTypeDefinition'; + /** * React component PropTypes Definitions. Consider using this as a supplementary * measure with `NavigationTypeDefinition`. This helps to capture the propType @@ -55,6 +59,7 @@ const layout = PropTypes.shape({ const scene = PropTypes.shape({ index: PropTypes.number.isRequired, isStale: PropTypes.bool.isRequired, + key: PropTypes.string.isRequired, navigationState, }); @@ -84,8 +89,30 @@ const panHandlers = PropTypes.shape({ onStartShouldSetResponderCapture: PropTypes.func.isRequired, }); +/** + * Helper function that extracts the props needed for scene renderer. + */ +function extractSceneRendererProps( + props: NavigationSceneRendererProps, +): NavigationSceneRendererProps { + return { + layout: props.layout, + navigationState: props.navigationState, + onNavigate: props.onNavigate, + position: props.position, + scene: props.scene, + scenes: props.scenes, + }; +} + module.exports = { + // helpers + extractSceneRendererProps, + + // Bundled propTypes. SceneRenderer, + + // propTypes action, navigationParentState, navigationState, diff --git a/Libraries/NavigationExperimental/NavigationTypeDefinition.js b/Libraries/NavigationExperimental/NavigationTypeDefinition.js index 694991ee7ca..907c361a94f 100644 --- a/Libraries/NavigationExperimental/NavigationTypeDefinition.js +++ b/Libraries/NavigationExperimental/NavigationTypeDefinition.js @@ -107,3 +107,8 @@ export type NavigationReducer = ( export type NavigationSceneRenderer = ( props: NavigationSceneRendererProps, ) => ?ReactElement; + +export type NavigationStyleInterpolator = ( + props: NavigationSceneRendererProps, +) => Object; +