NavigationCardStack - Add card stack item.

Summary: Add card stack item that moves from the right or the bottom.

Reviewed By: ericvicenti

Differential Revision: D2975659

fb-gh-sync-id: a04724943375ba0a9931eafb2aa82d6d8c31acfe
shipit-source-id: a04724943375ba0a9931eafb2aa82d6d8c31acfe
This commit is contained in:
Hedger Wang 2016-03-01 09:43:35 -08:00 committed by Facebook Github Bot 5
parent cbc0e21926
commit 0db22f184d
4 changed files with 309 additions and 19 deletions

View File

@ -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 (
<NavigationCardStack
style={styles.main}
renderScene={this._renderScene}
direction={this.state.isHorizontal ? 'horizontal' : 'vertical'}
navigationState={this.state.navigationState}
renderScene={this._renderScene}
style={styles.main}
/>
);
}
_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 (
<ScrollView style={styles.scrollView}>
<NavigationExampleRow
text={JSON.stringify(props.navigationState)}
text={
this.state.isHorizontal ?
'direction = "horizontal"' :
'direction = "vertical"'
}
onPress={this._toggleDirection}
/>
<NavigationExampleRow
text={'route = ' + props.navigationState.key}
/>
<NavigationExampleRow
text="Push Route"
@ -100,6 +109,12 @@ class NavigationCardStackExample extends React.Component {
</ScrollView>
);
}
_toggleDirection() {
this.setState({
isHorizontal: !this.state.isHorizontal,
});
}
}
const styles = StyleSheet.create({

View File

@ -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 (
<NavigationAnimatedView
navigationState={this.props.navigationState}
style={[styles.animatedView, this.props.style]}
renderOverlay={this.props.renderOverlay}
renderScene={this._renderScene}
setTiming={this._setTiming}
style={[styles.animatedView, this.props.style]}
/>
);
}
@ -81,33 +96,48 @@ class NavigationCardStack extends React.Component {
const {
index,
layout,
navigationParentState,
navigationState,
position,
} = props;
return (
<NavigationCard
key={navigationState.key}
<NavigationCardStackItem
direction={this.props.direction}
index={index}
navigationState={navigationParentState}
key={navigationState.key}
layout={layout}
navigationState={navigationState}
position={position}
layout={layout}>
{this.props.renderScene(props)}
</NavigationCard>
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,

View File

@ -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 <NavigationCardStack />.
*/
class NavigationCardStackItem extends React.Component {
props: Props;
state: State;
_calculateState: (t: Layout) => State;
_layoutListeners: Array<AmimatedValueSubscription>;
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 (
<Animated.View
style={[styles.main, animatedStyle]}>
{this.props.renderScene(this.props)}
</Animated.View>
);
}
_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;

View File

@ -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(),