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';
const NavigationExampleRow = require('./NavigationExampleRow');
const NavigationRootContainer = require('NavigationRootContainer');
const React = require('react-native');
const {
@ -25,62 +26,68 @@ const {
const NavigationCardStack = NavigationExperimental.CardStack;
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 {
constructor(props, context) {
super(props, context);
this.state = this._getInitialState();
this._renderNavigation = this._renderNavigation.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.state = {isHorizontal: true};
}
render() {
return (
<NavigationRootContainer
reducer={ExampleReducer}
renderNavigation={this._renderNavigation}
style={styles.main}
/>
);
}
_renderNavigation(navigationState, onNavigate) {
return (
<NavigationCardStack
direction={this.state.isHorizontal ? 'horizontal' : 'vertical'}
navigationState={this.state.navigationState}
navigationState={navigationState}
onNavigate={onNavigate}
renderScene={this._renderScene}
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) {
const {navigationParentState, onNavigate} = props;
return (
<ScrollView style={styles.scrollView}>
<NavigationExampleRow
@ -96,11 +103,20 @@ class NavigationCardStackExample extends React.Component {
/>
<NavigationExampleRow
text="Push Route"
onPress={this._push}
onPress={() => {
onNavigate({
type: 'push',
key: 'Route ' + navigationParentState.children.length,
});
}}
/>
<NavigationExampleRow
text="Pop Route"
onPress={this._pop}
onPress={() => {
onNavigate({
type: 'pop',
});
}}
/>
<NavigationExampleRow
text="Exit Card Stack Example"
@ -115,6 +131,12 @@ class NavigationCardStackExample extends React.Component {
isHorizontal: !this.state.isHorizontal,
});
}
_onNavigate(action) {
if (action && action.type === 'back') {
this._pop();
}
}
}
const styles = StyleSheet.create({

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

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

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

@ -29,33 +29,41 @@
const Animated = require('Animated');
const NavigationContainer = require('NavigationContainer');
const NavigationLinearPanResponder = require('NavigationLinearPanResponder');
const React = require('React');
const ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin');
const StyleSheet = require('StyleSheet');
const View = require('View');
const ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin');
const {PropTypes} = React;
const {Directions} = NavigationLinearPanResponder;
import type {
NavigationParentState,
} from 'NavigationStateUtils';
import type {
Layout,
Position,
NavigationStateRenderer,
} from 'NavigationAnimatedView';
import type {
Direction,
OnNavigateHandler,
} from 'NavigationLinearPanResponder';
type AnimatedValue = Animated.Value;
type Props = {
direction: string,
direction: Direction,
index: number;
layout: Layout;
navigationState: NavigationParentState;
position: Position;
renderScene: NavigationStateRenderer;
navigationParentState: NavigationParentState,
navigationState: NavigationParentState,
position: Position,
onNavigate: ?OnNavigateHandler,
renderScene: NavigationStateRenderer,
};
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 />.
*/
@ -119,9 +160,8 @@ class NavigationCardStackItem extends React.Component {
const {
direction,
index,
navigationState,
navigationParentState,
position,
layout,
} = this.props;
const {
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 (
<Animated.View
{...panHandlers}
style={[styles.main, animatedStyle]}>
{this.props.renderScene(this.props)}
</Animated.View>
@ -200,16 +248,12 @@ class NavigationCardStackItem extends React.Component {
}
}
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,
navigationParentState: PropTypes.object.isRequired,
position: PropTypes.object.isRequired,
renderScene: PropTypes.func.isRequired,
};
@ -218,10 +262,6 @@ NavigationCardStackItem.defaultProps = {
direction: Directions.HORIZONTAL,
};
NavigationCardStackItem = NavigationContainer.create(NavigationCardStackItem);
NavigationCardStackItem.Directions = Directions;
const styles = StyleSheet.create({
main: {
backgroundColor: '#E9E9EF',
@ -237,6 +277,4 @@ const styles = StyleSheet.create({
},
});
module.exports = NavigationCardStackItem;
module.exports = NavigationContainer.create(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,
// The state of the the containing navigation view.
navigationParentState: NavigationParentState,
onNavigate: (action: any) => void,
};
type NavigationStateRenderer = (
@ -100,11 +102,12 @@ type TimingSetter = (
) => void;
type Props = {
navigationState: NavigationParentState;
renderScene: NavigationStateRenderer;
renderOverlay: ?NavigationStateRenderer;
style: any;
setTiming: ?TimingSetter;
navigationState: NavigationParentState,
onNavigate: (action: any) => void,
renderScene: NavigationStateRenderer,
renderOverlay: ?NavigationStateRenderer,
style: any,
setTiming: ?TimingSetter,
};
class NavigationAnimatedView extends React.Component {
@ -224,18 +227,23 @@ class NavigationAnimatedView extends React.Component {
layout: this._getLayout(),
navigationParentState: this.props.navigationState,
navigationState: scene.state,
onNavigate: this.props.onNavigate,
position: this.state.position,
});
}
_renderOverlay() {
const {renderOverlay} = this.props;
const {
onNavigate,
renderOverlay,
navigationState,
} = this.props;
if (renderOverlay) {
const parentState = this.props.navigationState;
return renderOverlay({
index: parentState.index,
index: navigationState.index,
layout: this._getLayout(),
navigationParentState: parentState,
navigationState: parentState.children[parentState.index],
navigationParentState: navigationState,
navigationState: navigationState.children[navigationState.index],
onNavigate: onNavigate,
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;