From 4f8668b11092de84a911f92c1e0b19cfedbb8a33 Mon Sep 17 00:00:00 2001 From: Hedger Wang Date: Wed, 23 Mar 2016 12:49:28 -0700 Subject: [PATCH] Support animation and gesture for Pager. Summary: We need to support animation and gesture for Pager. Reviewed By: ericvicenti Differential Revision: D3066596 fb-gh-sync-id: 1c1a3d34b4298b4b0dd158f817057ae22dea72f4 shipit-source-id: 1c1a3d34b4298b4b0dd158f817057ae22dea72f4 --- .../NavigationExperimental/NavigationCard.js | 17 +- .../NavigationCardStack.js | 23 +- .../NavigationCardStackPanResponder.js} | 29 ++- .../NavigationCardStackStyleInterpolator.js | 20 +- .../NavigationLegacyNavigator.js | 81 ++++--- .../NavigationPagerPanResponder.js | 222 ++++++++++++++++++ .../NavigationPagerStyleInterpolator.js | 84 +++++++ .../NavigationExperimental.js | 8 - 8 files changed, 424 insertions(+), 60 deletions(-) rename Libraries/{NavigationExperimental/NavigationLinearPanResponder.js => CustomComponents/NavigationExperimental/NavigationCardStackPanResponder.js} (88%) create mode 100644 Libraries/CustomComponents/NavigationExperimental/NavigationPagerPanResponder.js create mode 100644 Libraries/CustomComponents/NavigationExperimental/NavigationPagerStyleInterpolator.js diff --git a/Libraries/CustomComponents/NavigationExperimental/NavigationCard.js b/Libraries/CustomComponents/NavigationExperimental/NavigationCard.js index 6e3d597f27c..e023281d66d 100644 --- a/Libraries/CustomComponents/NavigationExperimental/NavigationCard.js +++ b/Libraries/CustomComponents/NavigationExperimental/NavigationCard.js @@ -33,9 +33,11 @@ 'use strict'; const Animated = require('Animated'); +const NavigationCardStackPanResponder = require('NavigationCardStackPanResponder'); const NavigationCardStackStyleInterpolator = require('NavigationCardStackStyleInterpolator'); const NavigationContainer = require('NavigationContainer'); -const NavigationLinearPanResponder = require('NavigationLinearPanResponder'); +const NavigationPagerPanResponder = require('NavigationPagerPanResponder'); +const NavigationPagerStyleInterpolator = require('NavigationPagerStyleInterpolator'); const NavigationPropTypes = require('NavigationPropTypes'); const React = require('react-native'); const ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin'); @@ -91,7 +93,7 @@ class NavigationCard extends React.Component { } if (panHandlers === undefined) { // fall back to default pan handlers. - panHandlers = NavigationLinearPanResponder.forHorizontal(props); + panHandlers = NavigationCardStackPanResponder.forHorizontal(props); } return ( @@ -119,4 +121,13 @@ const styles = StyleSheet.create({ }, }); -module.exports = NavigationContainer.create(NavigationCard); + +const NavigationCardContainer = NavigationContainer.create(NavigationCard); + +// Export these buil-in interaction modules. +NavigationCardContainer.CardStackPanResponder = NavigationCardStackPanResponder; +NavigationCardContainer.CardStackStyleInterpolator = NavigationCardStackStyleInterpolator; +NavigationCardContainer.PagerPanResponder = NavigationPagerPanResponder; +NavigationCardContainer.PagerStyleInterpolator = NavigationPagerStyleInterpolator; + +module.exports = NavigationCardContainer; diff --git a/Libraries/CustomComponents/NavigationExperimental/NavigationCardStack.js b/Libraries/CustomComponents/NavigationExperimental/NavigationCardStack.js index 50388493751..2f153d2d7d6 100644 --- a/Libraries/CustomComponents/NavigationExperimental/NavigationCardStack.js +++ b/Libraries/CustomComponents/NavigationExperimental/NavigationCardStack.js @@ -37,7 +37,7 @@ const NavigationAnimatedView = require('NavigationAnimatedView'); const NavigationCard = require('NavigationCard'); const NavigationCardStackStyleInterpolator = require('NavigationCardStackStyleInterpolator'); const NavigationContainer = require('NavigationContainer'); -const NavigationLinearPanResponder = require('NavigationLinearPanResponder'); +const NavigationCardStackPanResponder = require('NavigationCardStackPanResponder'); const NavigationPropTypes = require('NavigationPropTypes'); const React = require('React'); const ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin'); @@ -46,7 +46,7 @@ const StyleSheet = require('StyleSheet'); const emptyFunction = require('fbjs/lib/emptyFunction'); const {PropTypes} = React; -const {Directions} = NavigationLinearPanResponder; +const {Directions} = NavigationCardStackPanResponder; import type { NavigationAnimatedValue, @@ -58,7 +58,7 @@ import type { import type { NavigationGestureDirection, -} from 'NavigationLinearPanResponder'; +} from 'NavigationCardStackPanResponder'; type Props = { direction: NavigationGestureDirection, @@ -80,7 +80,18 @@ const defaultProps = { }; /** - * A controlled navigation view that renders a list of cards. + * A controlled navigation view that renders a stack of cards. + * + * +------------+ + * +-+ | + * +-+ | | + * | | | | + * | | | Focused | + * | | | Card | + * | | | | + * +-+ | | + * +-+ | + * +------------+ */ class NavigationCardStack extends React.Component { _applyAnimation: NavigationAnimationSetter; @@ -123,8 +134,8 @@ class NavigationCardStack extends React.Component { NavigationCardStackStyleInterpolator.forHorizontal(props); const panHandlers = isVertical ? - NavigationLinearPanResponder.forVertical(props) : - NavigationLinearPanResponder.forHorizontal(props); + NavigationCardStackPanResponder.forVertical(props) : + NavigationCardStackPanResponder.forHorizontal(props); return ( , }; -function getConfigPopDirection(config: any): ?string { - if (config && config.gestures && config.gestures.pop) { - const direction = config.gestures.pop.direction; - return direction ? String(direction) : null; - } - - return null; -} - const RouteStack = NavigationLegacyNavigatorRouteStack; /** @@ -108,6 +102,7 @@ class NavigationLegacyNavigator extends React.Component { _renderScene: NavigationSceneRenderer; _routeFocused: any; _routeToFocus: any; + _onNavigate: NavigationActionCaller; _stack: NavigationLegacyNavigatorRouteStack; _useAnimation: boolean; @@ -206,6 +201,7 @@ class NavigationLegacyNavigator extends React.Component { componentWillMount(): void { this._applyAnimation = this._applyAnimation.bind(this); + this._onNavigate = this._onNavigate.bind(this); this._onNavigationBarRef = this._onNavigationBarRef.bind(this); this._onPositionChange = this._onPositionChange.bind(this); this._renderCard = this._renderCard.bind(this); @@ -240,6 +236,7 @@ class NavigationLegacyNavigator extends React.Component { { const {scene} = props; const {configureScene} = this.props; - let isVertical = false; + // Default getters for style and pan responders. + let styleGetter = NavigationCardStackStyleInterpolator.forHorizontal; + let panResponderGetter = NavigationCardStackPanResponder.forHorizontal; if (configureScene) { const route = RouteStack.getRouteByNavigationState(scene.navigationState); const config = configureScene(route, this.state.routeStack); - const direction = getConfigPopDirection(config); - switch (direction) { - case 'left-to-right': - // default. - break; - - case 'top-to-bottom': - isVertical = true; - break; - - default: - // unsupported config. - if (__DEV__) { - console.warn('unsupported scene configuration %s', direction); - } + if (config) { + const gestures = config.gestures || {}; + if (gestures.pop && gestures.pop.direction === 'left-to-right') { + // pass, will use default getters. + } else if (gestures.pop && gestures.pop.direction === 'top-to-bottom') { + styleGetter = NavigationCardStackStyleInterpolator.forVertical; + panResponderGetter = NavigationCardStackPanResponder.forVertical; + } else if ( + gestures.jumpBack && + gestures.jumpForward && + gestures.jumpBack.direction === 'left-to-right' && + gestures.jumpForward.direction === 'right-to-left' + ) { + styleGetter = NavigationPagerStyleInterpolator.forHorizontal; + panResponderGetter = NavigationPagerPanResponder.forHorizontal; + } else if (__DEV__) { + console.warn('unsupported scene configuration', config); + } } } - const style = isVertical ? - NavigationCardStackStyleInterpolator.forVertical(props) : - NavigationCardStackStyleInterpolator.forHorizontal(props); - - const panHandlers = isVertical ? - NavigationLinearPanResponder.forVertical(props) : - NavigationLinearPanResponder.forHorizontal(props); + const style = styleGetter(props); + const panHandlers = panResponderGetter(props); return ( { this.navigationContext.emit('didfocus', {route: route}); this.props.onDidFocus && this.props.onDidFocus(route); } + + _onNavigate(action: any): void { + switch (action) { + case NavigationCardStackPanResponder.Actions.BACK: + this.pop(); + break; + case NavigationPagerPanResponder.Actions.JUMP_BACK: + this.jumpBack(); + break; + case NavigationPagerPanResponder.Actions.JUMP_FORWARD: + this.jumpForward(); + break; + default: + if (__DEV__) { + console.warn('unsupported gesture action', action); + } + } + } } // Legacy static members. diff --git a/Libraries/CustomComponents/NavigationExperimental/NavigationPagerPanResponder.js b/Libraries/CustomComponents/NavigationExperimental/NavigationPagerPanResponder.js new file mode 100644 index 00000000000..b4ed5420cff --- /dev/null +++ b/Libraries/CustomComponents/NavigationExperimental/NavigationPagerPanResponder.js @@ -0,0 +1,222 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule NavigationPagerPanResponder + * @flow + * @typechecks + */ +'use strict'; + +const Animated = require('Animated'); +const NavigationAbstractPanResponder = require('NavigationAbstractPanResponder'); +const NavigationCardStackPanResponder = require('NavigationCardStackPanResponder'); + +const clamp = require('clamp'); + +import type { + NavigationPanPanHandlers, + NavigationSceneRendererProps, +} from 'NavigationTypeDefinition'; + +import type { + NavigationGestureDirection, +} from 'NavigationCardStackPanResponder'; + + + +/** + * Primitive gesture directions. + */ +const { + ANIMATION_DURATION, + DISTANCE_THRESHOLD, + POSITION_THRESHOLD, + RESPOND_THRESHOLD, + Directions, +} = NavigationCardStackPanResponder; + +/** + * Primitive gesture actions. + */ +const Actions = { + JUMP_BACK: {type: 'jump_back'}, + JUMP_FORWARD: {type: 'jump_forward'}, +}; + +/** + * Pan responder that handles gesture for a card in the cards list. + * + * +-------------+-------------+-------------+ + * | | | | + * | | | | + * | | | | + * | Next | Focused | Previous | + * | Card | Card | Card | + * | | | | + * | | | | + * | | | | + * +-------------+-------------+-------------+ + */ +class NavigationPagerPanResponder extends NavigationAbstractPanResponder { + + _isResponding: boolean; + _isVertical: boolean; + _props: NavigationSceneRendererProps; + _startValue: number; + + constructor( + direction: NavigationGestureDirection, + props: NavigationSceneRendererProps, + ) { + super(); + this._isResponding = false; + this._isVertical = direction === Directions.VERTICAL; + this._props = props; + this._startValue = 0; + } + + onMoveShouldSetPanResponder(event: any, gesture: any): boolean { + const props = this._props; + + if (props.navigationState.index !== props.scene.index) { + return false; + } + + const layout = props.layout; + const isVertical = this._isVertical; + const axis = isVertical ? 'dy' : 'dx'; + const index = props.navigationState.index; + 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._props.position.stopAnimation((value: number) => { + this._isResponding = true; + this._startValue = value; + }); + } + + onPanResponderMove(event: any, gesture: any): void { + if (!this._isResponding) { + return; + } + + const { + layout, + navigationState, + position, + scenes, + } = this._props; + + const isVertical = this._isVertical; + const axis = isVertical ? 'dy' : 'dx'; + const index = navigationState.index; + const distance = isVertical ? + layout.height.__getValue() : + layout.width.__getValue(); + + const prevIndex = Math.max( + 0, + index - 1, + ); + + const nextIndex = Math.min( + index + 1, + scenes.length - 1, + ); + + const value = clamp( + prevIndex, + this._startValue - (gesture[axis] / distance), + nextIndex, + ); + + position.setValue(value); + } + + onPanResponderRelease(event: any, gesture: any): void { + if (!this._isResponding) { + return; + } + + this._isResponding = false; + + const { + navigationState, + onNavigate, + position, + } = this._props; + + const isVertical = this._isVertical; + const axis = isVertical ? 'dy' : 'dx'; + const index = navigationState.index; + const distance = gesture[axis]; + + position.stopAnimation((value: number) => { + this._reset(); + if ( + distance > DISTANCE_THRESHOLD || + value <= index - POSITION_THRESHOLD + ) { + onNavigate(Actions.JUMP_BACK); + return; + } + + if ( + distance < -DISTANCE_THRESHOLD || + value >= index + POSITION_THRESHOLD + ) { + onNavigate(Actions.JUMP_FORWARD); + } + }); + } + + onPanResponderTerminate(): void { + this._isResponding = false; + this._reset(); + } + + _reset(): void { + const props = this._props; + Animated.timing( + props.position, + { + toValue: props.navigationState.index, + duration: ANIMATION_DURATION, + } + ).start(); + } +} + +function createPanHandlers( + direction: NavigationGestureDirection, + props: NavigationSceneRendererProps, +): NavigationPanPanHandlers { + const responder = new NavigationPagerPanResponder(direction, props); + return responder.panHandlers; +} + +function forHorizontal( + props: NavigationSceneRendererProps, +): NavigationPanPanHandlers { + return createPanHandlers(Directions.HORIZONTAL, props); +} + +module.exports = { + Actions, + forHorizontal, +}; diff --git a/Libraries/CustomComponents/NavigationExperimental/NavigationPagerStyleInterpolator.js b/Libraries/CustomComponents/NavigationExperimental/NavigationPagerStyleInterpolator.js new file mode 100644 index 00000000000..934be25ac82 --- /dev/null +++ b/Libraries/CustomComponents/NavigationExperimental/NavigationPagerStyleInterpolator.js @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * 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 NavigationPagerStyleInterpolator + * @flow + */ +'use strict'; + +import type { + NavigationSceneRendererProps, +} from 'NavigationTypeDefinition'; + +/** + * Utility that builds the style for the card in the cards list. + * + * +-------------+-------------+-------------+ + * | | | | + * | | | | + * | | | | + * | Next | Focused | Previous | + * | Card | Card | Card | + * | | | | + * | | | | + * | | | | + * +-------------+-------------+-------------+ + */ + +function forHorizontal(props: NavigationSceneRendererProps): Object { + const { + layout, + position, + scene, + } = props; + + const index = scene.index; + const inputRange = [index - 1, index, index + 1]; + + const width = layout.initWidth; + const translateX = position.interpolate({ + inputRange, + outputRange: [width, 0, -width], + }); + + return { + opacity : 1, + shadowColor: 'transparent', + shadowRadius: 0, + transform: [ + { scale: 1 }, + { translateX }, + { translateY: 0 }, + ], + }; +} + +module.exports = { + forHorizontal, +}; diff --git a/Libraries/NavigationExperimental/NavigationExperimental.js b/Libraries/NavigationExperimental/NavigationExperimental.js index e30fa3a8380..ca6bb627d55 100644 --- a/Libraries/NavigationExperimental/NavigationExperimental.js +++ b/Libraries/NavigationExperimental/NavigationExperimental.js @@ -14,11 +14,9 @@ const NavigationAnimatedView = require('NavigationAnimatedView'); const NavigationCard = require('NavigationCard'); const NavigationCardStack = require('NavigationCardStack'); -const NavigationCardStackStyleInterpolator = require('NavigationCardStackStyleInterpolator'); const NavigationContainer = require('NavigationContainer'); const NavigationHeader = require('NavigationHeader'); const NavigationLegacyNavigator = require('NavigationLegacyNavigator'); -const NavigationLinearPanResponder = require('NavigationLinearPanResponder'); const NavigationReducer = require('NavigationReducer'); const NavigationRootContainer = require('NavigationRootContainer'); const NavigationStateUtils = require('NavigationStateUtils'); @@ -42,12 +40,6 @@ const NavigationExperimental = { CardStack: NavigationCardStack, Header: NavigationHeader, LegacyNavigator: NavigationLegacyNavigator, - - // Animations Style Interpolators: - CardStackStyleInterpolator: NavigationCardStackStyleInterpolator, - - // Interactions: - LinearPanResponder: NavigationLinearPanResponder, }; module.exports = NavigationExperimental;