Add gesture handling for the card stack.

Reviewed By: ericvicenti

Differential Revision: D2995958

fb-gh-sync-id: f66759440b03072b650a572f011cadd06a0180d2
shipit-source-id: f66759440b03072b650a572f011cadd06a0180d2
This commit is contained in:
Hedger Wang 2016-03-01 18:44:58 -08:00 коммит произвёл Facebook Github Bot 2
Родитель 85801ef874
Коммит caac520952
6 изменённых файлов: 372 добавлений и 74 удалений

Просмотреть файл

@ -14,6 +14,7 @@
'use strict'; 'use strict';
const NavigationExampleRow = require('./NavigationExampleRow'); const NavigationExampleRow = require('./NavigationExampleRow');
const NavigationRootContainer = require('NavigationRootContainer');
const React = require('react-native'); const React = require('react-native');
const { const {
@ -25,62 +26,68 @@ const {
const NavigationCardStack = NavigationExperimental.CardStack; const NavigationCardStack = NavigationExperimental.CardStack;
const NavigationStateUtils = NavigationExperimental.StateUtils; const NavigationStateUtils = NavigationExperimental.StateUtils;
function reduceNavigationState(initialState) {
return (currentState, action) => {
switch (action.type) {
case 'RootContainerInitialAction':
return initialState;
case 'push':
return NavigationStateUtils.push(currentState, {key: action.key});
case 'back':
case 'pop':
return currentState.index > 0 ?
NavigationStateUtils.pop(currentState) :
currentState;
default:
return currentState;
}
};
}
const ExampleReducer = reduceNavigationState({
index: 0,
children: [{key: 'First Route'}],
});
class NavigationCardStackExample extends React.Component { class NavigationCardStackExample extends React.Component {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this.state = this._getInitialState();
this._renderNavigation = this._renderNavigation.bind(this);
this._renderScene = this._renderScene.bind(this); this._renderScene = this._renderScene.bind(this);
this._push = this._push.bind(this);
this._pop = this._pop.bind(this);
this._toggleDirection = this._toggleDirection.bind(this); this._toggleDirection = this._toggleDirection.bind(this);
this.state = {isHorizontal: true};
} }
render() { render() {
return (
<NavigationRootContainer
reducer={ExampleReducer}
renderNavigation={this._renderNavigation}
style={styles.main}
/>
);
}
_renderNavigation(navigationState, onNavigate) {
return ( return (
<NavigationCardStack <NavigationCardStack
direction={this.state.isHorizontal ? 'horizontal' : 'vertical'} direction={this.state.isHorizontal ? 'horizontal' : 'vertical'}
navigationState={this.state.navigationState} navigationState={navigationState}
onNavigate={onNavigate}
renderScene={this._renderScene} renderScene={this._renderScene}
style={styles.main} style={styles.main}
/> />
); );
} }
_getInitialState() {
const navigationState = {
index: 0,
children: [{key: 'First Route'}],
};
return {
isHorizontal: true,
navigationState,
};
}
_push() {
const state = this.state.navigationState;
const nextState = NavigationStateUtils.push(
state,
{key: 'Route ' + (state.index + 1)},
);
this.setState({
navigationState: nextState,
});
}
_pop() {
const state = this.state.navigationState;
const nextState = state.index > 0 ?
NavigationStateUtils.pop(state) :
state;
this.setState({
navigationState: nextState,
});
}
_renderScene(props) { _renderScene(props) {
const {navigationParentState, onNavigate} = props;
return ( return (
<ScrollView style={styles.scrollView}> <ScrollView style={styles.scrollView}>
<NavigationExampleRow <NavigationExampleRow
@ -96,11 +103,20 @@ class NavigationCardStackExample extends React.Component {
/> />
<NavigationExampleRow <NavigationExampleRow
text="Push Route" text="Push Route"
onPress={this._push} onPress={() => {
onNavigate({
type: 'push',
key: 'Route ' + navigationParentState.children.length,
});
}}
/> />
<NavigationExampleRow <NavigationExampleRow
text="Pop Route" text="Pop Route"
onPress={this._pop} onPress={() => {
onNavigate({
type: 'pop',
});
}}
/> />
<NavigationExampleRow <NavigationExampleRow
text="Exit Card Stack Example" text="Exit Card Stack Example"
@ -115,6 +131,12 @@ class NavigationCardStackExample extends React.Component {
isHorizontal: !this.state.isHorizontal, isHorizontal: !this.state.isHorizontal,
}); });
} }
_onNavigate(action) {
if (action && action.type === 'back') {
this._pop();
}
}
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({

Просмотреть файл

@ -31,6 +31,7 @@ const Animated = require('Animated');
const NavigationAnimatedView = require('NavigationAnimatedView'); const NavigationAnimatedView = require('NavigationAnimatedView');
const NavigationCardStackItem = require('NavigationCardStackItem'); const NavigationCardStackItem = require('NavigationCardStackItem');
const NavigationContainer = require('NavigationContainer'); const NavigationContainer = require('NavigationContainer');
const NavigationLinearPanResponder = require('NavigationLinearPanResponder');
const React = require('React'); const React = require('React');
const ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin'); const ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin');
const StyleSheet = require('StyleSheet'); const StyleSheet = require('StyleSheet');
@ -38,14 +39,13 @@ const StyleSheet = require('StyleSheet');
const emptyFunction = require('emptyFunction'); const emptyFunction = require('emptyFunction');
const {PropTypes} = React; const {PropTypes} = React;
const {Directions} = NavigationCardStackItem; const {Directions} = NavigationLinearPanResponder;
import type { import type {
NavigationParentState, NavigationParentState,
} from 'NavigationStateUtils'; } from 'NavigationStateUtils';
import type { import type {
Layout,
NavigationStateRenderer, NavigationStateRenderer,
NavigationStateRendererProps, NavigationStateRendererProps,
Position, Position,
@ -55,7 +55,7 @@ import type {
type Props = { type Props = {
direction: string, direction: string,
navigationState: NavigationParentState, navigationState: NavigationParentState,
renderOverlay: NavigationStateRenderer, renderOverlay: ?NavigationStateRenderer,
renderScene: NavigationStateRenderer, renderScene: NavigationStateRenderer,
}; };
@ -98,6 +98,7 @@ class NavigationCardStack extends React.Component {
layout, layout,
navigationState, navigationState,
position, position,
navigationParentState,
} = props; } = props;
return ( return (
@ -106,6 +107,7 @@ class NavigationCardStack extends React.Component {
index={index} index={index}
key={navigationState.key} key={navigationState.key}
layout={layout} layout={layout}
navigationParentState={navigationParentState}
navigationState={navigationState} navigationState={navigationState}
position={position} position={position}
renderScene={this.props.renderScene} renderScene={this.props.renderScene}
@ -136,8 +138,6 @@ NavigationCardStack.defaultProps = {
renderOverlay: emptyFunction.thatReturnsNull, renderOverlay: emptyFunction.thatReturnsNull,
}; };
NavigationCardStack.Directions = Directions;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
animatedView: { animatedView: {
flex: 1, flex: 1,

Просмотреть файл

@ -29,33 +29,41 @@
const Animated = require('Animated'); const Animated = require('Animated');
const NavigationContainer = require('NavigationContainer'); const NavigationContainer = require('NavigationContainer');
const NavigationLinearPanResponder = require('NavigationLinearPanResponder');
const React = require('React'); const React = require('React');
const ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin');
const StyleSheet = require('StyleSheet'); const StyleSheet = require('StyleSheet');
const View = require('View'); const View = require('View');
const ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin');
const {PropTypes} = React; const {PropTypes} = React;
const {Directions} = NavigationLinearPanResponder;
import type { import type {
NavigationParentState, NavigationParentState,
} from 'NavigationStateUtils'; } from 'NavigationStateUtils';
import type { import type {
Layout, Layout,
Position, Position,
NavigationStateRenderer, NavigationStateRenderer,
} from 'NavigationAnimatedView'; } from 'NavigationAnimatedView';
import type {
Direction,
OnNavigateHandler,
} from 'NavigationLinearPanResponder';
type AnimatedValue = Animated.Value; type AnimatedValue = Animated.Value;
type Props = { type Props = {
direction: string, direction: Direction,
index: number; index: number;
layout: Layout; layout: Layout;
navigationState: NavigationParentState; navigationParentState: NavigationParentState,
position: Position; navigationState: NavigationParentState,
renderScene: NavigationStateRenderer; position: Position,
onNavigate: ?OnNavigateHandler,
renderScene: NavigationStateRenderer,
}; };
type State = { type State = {
@ -78,6 +86,39 @@ class AmimatedValueSubscription {
} }
} }
/**
* Class that provides the required information for the
* `NavigationLinearPanResponder`. This class must implement
* the interface `NavigationLinearPanResponderDelegate`.
*/
class PanResponderDelegate {
_props : Props;
constructor(props: Props) {
this._props = props;
}
getDirection(): Direction {
return this._props.direction;
}
getIndex(): number {
return this._props.navigationParentState.index;
}
getLayout(): Layout {
return this._props.layout;
}
getPosition(): Position {
return this._props.position;
}
onNavigate(action: {type: string}): void {
this._props.onNavigate && this._props.onNavigate(action);
}
}
/** /**
* Component that renders the scene as card for the <NavigationCardStack />. * Component that renders the scene as card for the <NavigationCardStack />.
*/ */
@ -119,9 +160,8 @@ class NavigationCardStackItem extends React.Component {
const { const {
direction, direction,
index, index,
navigationState, navigationParentState,
position, position,
layout,
} = this.props; } = this.props;
const { const {
height, height,
@ -161,8 +201,16 @@ class NavigationCardStackItem extends React.Component {
], ],
}; };
let panHandlers = null;
if (navigationParentState.index === index) {
const delegate = new PanResponderDelegate(this.props);
const panResponder = new NavigationLinearPanResponder(delegate);
panHandlers = panResponder.panHandlers;
}
return ( return (
<Animated.View <Animated.View
{...panHandlers}
style={[styles.main, animatedStyle]}> style={[styles.main, animatedStyle]}>
{this.props.renderScene(this.props)} {this.props.renderScene(this.props)}
</Animated.View> </Animated.View>
@ -200,16 +248,12 @@ class NavigationCardStackItem extends React.Component {
} }
} }
const Directions = {
HORIZONTAL: 'horizontal',
VERTICAL: 'vertical',
};
NavigationCardStackItem.propTypes = { NavigationCardStackItem.propTypes = {
direction: PropTypes.oneOf([Directions.HORIZONTAL, Directions.VERTICAL]), direction: PropTypes.oneOf([Directions.HORIZONTAL, Directions.VERTICAL]),
index: PropTypes.number.isRequired, index: PropTypes.number.isRequired,
layout: PropTypes.object.isRequired, layout: PropTypes.object.isRequired,
navigationState: PropTypes.object.isRequired, navigationState: PropTypes.object.isRequired,
navigationParentState: PropTypes.object.isRequired,
position: PropTypes.object.isRequired, position: PropTypes.object.isRequired,
renderScene: PropTypes.func.isRequired, renderScene: PropTypes.func.isRequired,
}; };
@ -218,10 +262,6 @@ NavigationCardStackItem.defaultProps = {
direction: Directions.HORIZONTAL, direction: Directions.HORIZONTAL,
}; };
NavigationCardStackItem = NavigationContainer.create(NavigationCardStackItem);
NavigationCardStackItem.Directions = Directions;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
main: { main: {
backgroundColor: '#E9E9EF', backgroundColor: '#E9E9EF',
@ -237,6 +277,4 @@ const styles = StyleSheet.create({
}, },
}); });
module.exports = NavigationContainer.create(NavigationCardStackItem);
module.exports = NavigationCardStackItem;

Просмотреть файл

@ -0,0 +1,47 @@
/**
* Copyright 2004-present Facebook. All Rights Reserved.
*
* @providesModule NavigationAbstractPanResponder
* @flow
*/
'use strict';
const PanResponder = require('PanResponder');
const invariant = require('invariant');
const EmptyPanHandlers = {
onMoveShouldSetPanResponder: null,
onPanResponderGrant: null,
onPanResponderMove: null,
onPanResponderRelease: null,
onPanResponderTerminate: null,
};
/**
* Abstract class that defines the common interface of PanResponder that handles
* the gesture actions.
*/
class NavigationAbstractPanResponder {
panHandlers: Object;
constructor() {
const config = {};
Object.keys(EmptyPanHandlers).forEach(name => {
const fn: any = (this: any)[name];
invariant(
typeof fn === 'function',
'subclass of `NavigationAbstractPanResponder` must implement method %s',
name
);
config[name] = fn.bind(this);
}, this);
this.panHandlers = PanResponder.create(config).panHandlers;
}
}
module.exports = NavigationAbstractPanResponder;

Просмотреть файл

@ -87,6 +87,8 @@ type NavigationStateRendererProps = {
layout: Layout, layout: Layout,
// The state of the the containing navigation view. // The state of the the containing navigation view.
navigationParentState: NavigationParentState, navigationParentState: NavigationParentState,
onNavigate: (action: any) => void,
}; };
type NavigationStateRenderer = ( type NavigationStateRenderer = (
@ -100,11 +102,12 @@ type TimingSetter = (
) => void; ) => void;
type Props = { type Props = {
navigationState: NavigationParentState; navigationState: NavigationParentState,
renderScene: NavigationStateRenderer; onNavigate: (action: any) => void,
renderOverlay: ?NavigationStateRenderer; renderScene: NavigationStateRenderer,
style: any; renderOverlay: ?NavigationStateRenderer,
setTiming: ?TimingSetter; style: any,
setTiming: ?TimingSetter,
}; };
class NavigationAnimatedView extends React.Component { class NavigationAnimatedView extends React.Component {
@ -224,18 +227,23 @@ class NavigationAnimatedView extends React.Component {
layout: this._getLayout(), layout: this._getLayout(),
navigationParentState: this.props.navigationState, navigationParentState: this.props.navigationState,
navigationState: scene.state, navigationState: scene.state,
onNavigate: this.props.onNavigate,
position: this.state.position, position: this.state.position,
}); });
} }
_renderOverlay() { _renderOverlay() {
const {renderOverlay} = this.props; const {
onNavigate,
renderOverlay,
navigationState,
} = this.props;
if (renderOverlay) { if (renderOverlay) {
const parentState = this.props.navigationState;
return renderOverlay({ return renderOverlay({
index: parentState.index, index: navigationState.index,
layout: this._getLayout(), layout: this._getLayout(),
navigationParentState: parentState, navigationParentState: navigationState,
navigationState: parentState.children[parentState.index], navigationState: navigationState.children[navigationState.index],
onNavigate: onNavigate,
position: this.state.position, position: this.state.position,
}); });
} }

Просмотреть файл

@ -0,0 +1,183 @@
/**
* Copyright 2004-present Facebook. All Rights Reserved.
*
* @providesModule NavigationLinearPanResponder
* @flow
* @typechecks
*/
'use strict';
const Animated = require('Animated');
const NavigationAbstractPanResponder = require('NavigationAbstractPanResponder');
const clamp = require('clamp');
/**
* The duration of the card animation in milliseconds.
*/
const ANIMATION_DURATION = 250;
/**
* The threshold to invoke the `onNavigate` action.
* For instance, `1 / 3` means that moving greater than 1 / 3 of the width of
* the view will navigate.
*/
const POSITION_THRESHOLD = 1 / 3;
/**
* The threshold (in pixels) to start the gesture action.
*/
const RESPOND_THRESHOLD = 15;
/**
* The threshold (in speed) to finish the gesture action.
*/
const VELOCITY_THRESHOLD = 100;
/**
* Primitive gesture directions.
*/
const Directions = {
'HORIZONTAL': 'horizontal',
'VERTICAL': 'vertical',
};
/**
* Primitive gesture actions.
*/
const Actions = {
// The gesture to navigate backward.
// This is done by swiping from the left to the right or from the top to the
// bottom.
BACK: {type: 'back'},
};
import type {
Layout,
Position,
} from 'NavigationAnimatedView';
export type OnNavigateHandler = (action: {type: string}) => void;
export type Direction = $Enum<typeof Directions>;
/**
* The type interface of the object that provides the information required by
* NavigationLinearPanResponder.
*/
export type NavigationLinearPanResponderDelegate = {
getDirection: () => Direction;
getIndex: () => number,
getLayout: () => Layout,
getPosition: () => Position,
onNavigate: OnNavigateHandler,
};
/**
* Pan responder that handles the One-dimensional gesture (horizontal or
* vertical).
*/
class NavigationLinearPanResponder extends NavigationAbstractPanResponder {
static Actions: Object;
static Directions: Object;
_isResponding: boolean;
_startValue: number;
_delegate: NavigationLinearPanResponderDelegate;
constructor(delegate: NavigationLinearPanResponderDelegate) {
super();
this._isResponding = false;
this._startValue = 0;
this._delegate = delegate;
}
onMoveShouldSetPanResponder(event: any, gesture: any): boolean {
const delegate = this._delegate;
const layout = delegate.getLayout();
const isVertical = delegate.getDirection() === Directions.VERTICAL;
const axis = isVertical ? 'dy' : 'dx';
const index = delegate.getIndex();
const distance = isVertical ?
layout.height.__getValue() :
layout.width.__getValue();
return (
Math.abs(gesture[axis]) > RESPOND_THRESHOLD &&
distance > 0 &&
index > 0
);
}
onPanResponderGrant(): void {
this._isResponding = false;
this._delegate.getPosition().stopAnimation((value: number) => {
this._isResponding = true;
this._startValue = value;
});
}
onPanResponderMove(event: any, gesture: any): void {
if (!this._isResponding) {
return;
}
const delegate = this._delegate;
const layout = delegate.getLayout();
const isVertical = delegate.getDirection() === Directions.VERTICAL;
const axis = isVertical ? 'dy' : 'dx';
const index = delegate.getIndex();
const distance = isVertical ?
layout.height.__getValue() :
layout.width.__getValue();
const value = clamp(
index - 1,
this._startValue - (gesture[axis] / distance),
index
);
this._delegate.getPosition().setValue(value);
}
onPanResponderRelease(event: any, gesture: any): void {
if (!this._isResponding) {
return;
}
this._isResponding = false;
const delegate = this._delegate;
const isVertical = delegate.getDirection() === Directions.VERTICAL;
const axis = isVertical ? 'dy' : 'dx';
const index = delegate.getIndex();
const velocity = gesture[axis];
delegate.getPosition().stopAnimation((value: number) => {
this._reset();
if (velocity > VELOCITY_THRESHOLD || value <= index - POSITION_THRESHOLD) {
delegate.onNavigate(Actions.BACK);
}
});
}
onPanResponderTerminate(): void {
this._isResponding = false;
this._reset();
}
_reset(): void {
Animated.timing(
this._delegate.getPosition(),
{
toValue: this._delegate.getIndex(),
duration: ANIMATION_DURATION,
}
).start();
}
}
NavigationLinearPanResponder.Actions = Actions;
NavigationLinearPanResponder.Directions = Directions;
module.exports = NavigationLinearPanResponder;