diff --git a/Examples/UIExplorer/NavigationExperimental/NavigationCardStackExample.js b/Examples/UIExplorer/NavigationExperimental/NavigationCardStackExample.js index 94a6735d0..ae08ac75d 100644 --- a/Examples/UIExplorer/NavigationExperimental/NavigationCardStackExample.js +++ b/Examples/UIExplorer/NavigationExperimental/NavigationCardStackExample.js @@ -33,35 +33,36 @@ class NavigationCardStackExample extends React.Component { this._renderScene = this._renderScene.bind(this); this._push = this._push.bind(this); this._pop = this._pop.bind(this); + this._toggleDirection = this._toggleDirection.bind(this); } render() { return ( ); } _getInitialState() { - const route = {key: 'First Route'}; const navigationState = { index: 0, - children: [route], + children: [{key: 'First Route'}], }; return { + isHorizontal: true, navigationState, }; } _push() { const state = this.state.navigationState; - const nextRoute = {key: 'Route ' + (state.index + 1)}; const nextState = NavigationStateUtils.push( state, - nextRoute, + {key: 'Route ' + (state.index + 1)}, ); this.setState({ navigationState: nextState, @@ -83,7 +84,15 @@ class NavigationCardStackExample extends React.Component { return ( + ); } + + _toggleDirection() { + this.setState({ + isHorizontal: !this.state.isHorizontal, + }); + } } const styles = StyleSheet.create({ diff --git a/Libraries/CustomComponents/NavigationExperimental/NavigationCardStack.js b/Libraries/CustomComponents/NavigationExperimental/NavigationCardStack.js index b4e53a63a..dbb81eff8 100644 --- a/Libraries/CustomComponents/NavigationExperimental/NavigationCardStack.js +++ b/Libraries/CustomComponents/NavigationExperimental/NavigationCardStack.js @@ -27,19 +27,21 @@ */ 'use strict'; +const Animated = require('Animated'); const NavigationAnimatedView = require('NavigationAnimatedView'); -const NavigationCard = require('NavigationCard'); +const NavigationCardStackItem = require('NavigationCardStackItem'); const NavigationContainer = require('NavigationContainer'); const React = require('React'); +const ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin'); const StyleSheet = require('StyleSheet'); const emptyFunction = require('emptyFunction'); const {PropTypes} = React; +const {Directions} = NavigationCardStackItem; import type { NavigationParentState, - NavigationState, } from 'NavigationStateUtils'; import type { @@ -47,9 +49,11 @@ import type { NavigationStateRenderer, NavigationStateRendererProps, Position, + TimingSetter, } from 'NavigationAnimatedView'; type Props = { + direction: string, navigationState: NavigationParentState, renderOverlay: NavigationStateRenderer, renderScene: NavigationStateRenderer, @@ -60,19 +64,30 @@ type Props = { */ class NavigationCardStack extends React.Component { _renderScene : NavigationStateRenderer; + _setTiming: TimingSetter; constructor(props: Props, context: any) { super(props, context); this._renderScene = this._renderScene.bind(this); + this._setTiming = this._setTiming.bind(this); + } + + shouldComponentUpdate(nextProps: Object, nextState: Object): boolean { + return ReactComponentWithPureRenderMixin.shouldComponentUpdate.call( + this, + nextProps, + nextState + ); } render(): ReactElement { return ( ); } @@ -81,33 +96,48 @@ class NavigationCardStack extends React.Component { const { index, layout, - navigationParentState, navigationState, position, } = props; + return ( - - {this.props.renderScene(props)} - + renderScene={this.props.renderScene} + /> ); } + + _setTiming(position: Position, navigationState: NavigationParentState): void { + Animated.timing( + position, + { + duration: 500, + toValue: navigationState.index, + } + ).start(); + } } NavigationCardStack.propTypes = { + direction: PropTypes.oneOf([Directions.HORIZONTAL, Directions.VERTICAL]), navigationState: PropTypes.object.isRequired, renderOverlay: PropTypes.func, renderScene: PropTypes.func.isRequired, }; NavigationCardStack.defaultProps = { + direction: Directions.HORIZONTAL, renderOverlay: emptyFunction.thatReturnsNull, }; +NavigationCardStack.Directions = Directions; + const styles = StyleSheet.create({ animatedView: { flex: 1, diff --git a/Libraries/CustomComponents/NavigationExperimental/NavigationCardStackItem.js b/Libraries/CustomComponents/NavigationExperimental/NavigationCardStackItem.js new file mode 100644 index 000000000..ff9d5f268 --- /dev/null +++ b/Libraries/CustomComponents/NavigationExperimental/NavigationCardStackItem.js @@ -0,0 +1,242 @@ +/** + * Copyright (c) 2015, Facebook, Inc. All rights reserved. + * + * 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 NavigationCardStackItem + * @flow + */ +'use strict'; + +const Animated = require('Animated'); +const NavigationContainer = require('NavigationContainer'); +const React = require('React'); +const StyleSheet = require('StyleSheet'); +const View = require('View'); +const ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin'); + +const {PropTypes} = React; + +import type { + NavigationParentState, +} from 'NavigationStateUtils'; + + +import type { + Layout, + Position, + NavigationStateRenderer, +} from 'NavigationAnimatedView'; + +type AnimatedValue = Animated.Value; + +type Props = { + direction: string, + index: number; + layout: Layout; + navigationState: NavigationParentState; + position: Position; + renderScene: NavigationStateRenderer; +}; + +type State = { + hash: string, + height: number, + width: number, +}; + +class AmimatedValueSubscription { + _value: AnimatedValue; + _token: string; + + constructor(value: AnimatedValue, callback: Function) { + this._value = value; + this._token = value.addListener(callback); + } + + remove() { + this._value.removeListener(this._token); + } +} + +/** + * Component that renders the scene as card for the . + */ +class NavigationCardStackItem extends React.Component { + props: Props; + state: State; + _calculateState: (t: Layout) => State; + _layoutListeners: Array; + + constructor(props: Props, context: any) { + super(props, context); + + this._calculateState = this._calculateState.bind(this); + this.state = this._calculateState(props.layout); + this._layoutListeners = []; + } + + shouldComponentUpdate(nextProps: Object, nextState: Object): boolean { + return ReactComponentWithPureRenderMixin.shouldComponentUpdate.call( + this, + nextProps, + nextState + ); + } + + componentDidMount(): void { + this._applyLayout(this.props.layout); + } + + componentWillUnmount(): void { + this._layoutListeners.forEach(subscription => subscription.remove); + } + + componentWillReceiveProps(nextProps: Props): void { + this._applyLayout(nextProps.layout); + } + + render(): ReactElement { + const { + direction, + index, + navigationState, + position, + layout, + } = this.props; + const { + height, + width, + } = this.state; + + const isVertical = direction === 'vertical'; + const inputRange = [index - 1, index, index + 1]; + const animatedStyle = { + + opacity: position.interpolate({ + inputRange, + outputRange: [1, 1, 0.3], + }), + + transform: [ + { + scale: position.interpolate({ + inputRange, + outputRange: [1, 1, 0.95], + }), + }, + { + translateX: isVertical ? 0 : + position.interpolate({ + inputRange, + outputRange: [width, 0, -10], + }), + }, + { + translateY: !isVertical ? 0 : + position.interpolate({ + inputRange, + outputRange: [height, 0, -10], + }), + }, + ], + }; + + return ( + + {this.props.renderScene(this.props)} + + ); + } + + _calculateState(layout: Layout): State { + const width = layout.width.__getValue(); + const height = layout.height.__getValue(); + const hash = 'layout-' + width + '-' + height; + const state = { + height, + width, + hash, + }; + return state; + } + + _applyLayout(layout: Layout) { + this._layoutListeners.forEach(subscription => subscription.remove); + + this._layoutListeners.length = 0; + + const callback = this._applyLayout.bind(this, layout); + + this._layoutListeners.push( + new AmimatedValueSubscription(layout.width, callback), + new AmimatedValueSubscription(layout.height, callback), + ); + + const nextState = this._calculateState(layout); + if (nextState.hash !== this.state.hash) { + this.setState(nextState); + } + } +} + +const Directions = { + HORIZONTAL: 'horizontal', + VERTICAL: 'vertical', +}; + +NavigationCardStackItem.propTypes = { + direction: PropTypes.oneOf([Directions.HORIZONTAL, Directions.VERTICAL]), + index: PropTypes.number.isRequired, + layout: PropTypes.object.isRequired, + navigationState: PropTypes.object.isRequired, + position: PropTypes.object.isRequired, + renderScene: PropTypes.func.isRequired, +}; + +NavigationCardStackItem.defaultProps = { + direction: Directions.HORIZONTAL, +}; + +NavigationCardStackItem = NavigationContainer.create(NavigationCardStackItem); + +NavigationCardStackItem.Directions = Directions; + +const styles = StyleSheet.create({ + main: { + backgroundColor: '#E9E9EF', + bottom: 0, + left: 0, + position: 'absolute', + right: 0, + shadowColor: 'black', + shadowOffset: {width: 0, height: 0}, + shadowOpacity: 0.4, + shadowRadius: 10, + top: 0, + }, +}); + + + +module.exports = NavigationCardStackItem; diff --git a/Libraries/NavigationExperimental/NavigationAnimatedView.js b/Libraries/NavigationExperimental/NavigationAnimatedView.js index b19502166..bf3024ce8 100644 --- a/Libraries/NavigationExperimental/NavigationAnimatedView.js +++ b/Libraries/NavigationExperimental/NavigationAnimatedView.js @@ -115,8 +115,11 @@ class NavigationAnimatedView extends React.Component { props: Props; constructor(props) { super(props); - this._animatedHeight = new Animated.Value(0); - this._animatedWidth = new Animated.Value(0); + this._lastWidth = 0; + this._lastHeight = 0; + this._animatedHeight = new Animated.Value(this._lastHeight); + this._animatedWidth = new Animated.Value(this._lastWidth); + this.state = { position: new Animated.Value(this.props.navigationState.index), scenes: new Map(),