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:
Родитель
85801ef874
Коммит
caac520952
|
@ -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;
|
Загрузка…
Ссылка в новой задаче