From 5966ccbcad7780c2bc105b3f0c5a69959db188c3 Mon Sep 17 00:00:00 2001 From: Hedger Wang Date: Mon, 2 May 2016 12:56:46 -0700 Subject: [PATCH] better way to manage pointerEvents for NavigationCard. Summary: THis addresses the issue as reported at https://github.com/facebook/react-native/issues/6732 Use a higher order component `NavigationPointerEventsContainer` to manager the prop `pointerEvents` for `NavigationCard`. The idea is that the scene's content should not be interactive while the scene is transitioning. Reviewed By: ericvicenti Differential Revision: D3205106 fb-gh-sync-id: c0fd22e8c8b83a5952351c5a3a302b2fca5ba5de fbshipit-source-id: c0fd22e8c8b83a5952351c5a3a302b2fca5ba5de --- .../NavigationExperimental/NavigationCard.js | 69 ++++---- .../NavigationPointerEventsContainer.js | 156 ++++++++++++++++++ 2 files changed, 184 insertions(+), 41 deletions(-) create mode 100644 Libraries/CustomComponents/NavigationExperimental/NavigationPointerEventsContainer.js diff --git a/Libraries/CustomComponents/NavigationExperimental/NavigationCard.js b/Libraries/CustomComponents/NavigationExperimental/NavigationCard.js index a1a618a3a..157c359d1 100644 --- a/Libraries/CustomComponents/NavigationExperimental/NavigationCard.js +++ b/Libraries/CustomComponents/NavigationExperimental/NavigationCard.js @@ -38,6 +38,7 @@ const NavigationCardStackStyleInterpolator = require('NavigationCardStackStyleIn const NavigationContainer = require('NavigationContainer'); const NavigationPagerPanResponder = require('NavigationPagerPanResponder'); const NavigationPagerStyleInterpolator = require('NavigationPagerStyleInterpolator'); +const NavigationPointerEventsContainer = require('NavigationPointerEventsContainer'); const NavigationPropTypes = require('NavigationPropTypes'); const React = require('React'); const ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin'); @@ -51,26 +52,30 @@ import type { } from 'NavigationTypeDefinition'; type Props = NavigationSceneRendererProps & { - style: any, + onComponentRef: (ref: any) => void, panHandlers: ?NavigationPanPanHandlers, + pointerEvents: string, renderScene: NavigationSceneRenderer, + style: any, }; const {PropTypes} = React; -const propTypes = { - ...NavigationPropTypes.SceneRenderer, - style: PropTypes.any, - panHandlers: NavigationPropTypes.panHandlers, - renderScene: PropTypes.func.isRequired, -}; - /** * Component that renders the scene as card for the . */ class NavigationCard extends React.Component { props: Props; + static propTypes = { + ...NavigationPropTypes.SceneRenderer, + onComponentRef: PropTypes.func.isRequired, + panHandlers: NavigationPropTypes.panHandlers, + pointerEvents: PropTypes.string.isRequired, + renderScene: PropTypes.func.isRequired, + style: PropTypes.any, + }; + shouldComponentUpdate(nextProps: Props, nextState: any): boolean { return ReactComponentWithPureRenderMixin.shouldComponentUpdate.call( this, @@ -82,41 +87,25 @@ class NavigationCard extends React.Component { render(): ReactElement { const { panHandlers, + pointerEvents, renderScene, style, ...props, /* NavigationSceneRendererProps */ } = this.props; - let viewStyle = null; - if (style === undefined) { - // fall back to default style. - viewStyle = NavigationCardStackStyleInterpolator.forHorizontal(props); - } else { - viewStyle = style; - } + const viewStyle = style === undefined ? + NavigationCardStackStyleInterpolator.forHorizontal(props) : + style; - const { - navigationState, - scene, - } = props; - - const interactive = navigationState.index === scene.index && !scene.isStale; - const pointerEvents = interactive ? 'auto' : 'none'; - - let viewPanHandlers = null; - if (interactive) { - if (panHandlers === undefined) { - // fall back to default pan handlers. - viewPanHandlers = NavigationCardStackPanResponder.forHorizontal(props); - } else { - viewPanHandlers = panHandlers; - } - } + const viewPanHandlers = panHandlers === undefined ? + NavigationCardStackPanResponder.forHorizontal(props) : + panHandlers; return ( {renderScene(props)} @@ -124,8 +113,6 @@ class NavigationCard extends React.Component { } } -NavigationCard.propTypes = propTypes; - const styles = StyleSheet.create({ main: { backgroundColor: '#E9E9EF', @@ -141,13 +128,13 @@ const styles = StyleSheet.create({ }, }); - -const NavigationCardContainer = NavigationContainer.create(NavigationCard); +NavigationCard = NavigationPointerEventsContainer.create(NavigationCard); +NavigationCard = NavigationContainer.create(NavigationCard); // Export these buil-in interaction modules. -NavigationCardContainer.CardStackPanResponder = NavigationCardStackPanResponder; -NavigationCardContainer.CardStackStyleInterpolator = NavigationCardStackStyleInterpolator; -NavigationCardContainer.PagerPanResponder = NavigationPagerPanResponder; -NavigationCardContainer.PagerStyleInterpolator = NavigationPagerStyleInterpolator; +NavigationCard.CardStackPanResponder = NavigationCardStackPanResponder; +NavigationCard.CardStackStyleInterpolator = NavigationCardStackStyleInterpolator; +NavigationCard.PagerPanResponder = NavigationPagerPanResponder; +NavigationCard.PagerStyleInterpolator = NavigationPagerStyleInterpolator; -module.exports = NavigationCardContainer; +module.exports = NavigationCard; diff --git a/Libraries/CustomComponents/NavigationExperimental/NavigationPointerEventsContainer.js b/Libraries/CustomComponents/NavigationExperimental/NavigationPointerEventsContainer.js new file mode 100644 index 000000000..25905b3c6 --- /dev/null +++ b/Libraries/CustomComponents/NavigationExperimental/NavigationPointerEventsContainer.js @@ -0,0 +1,156 @@ +/** + * 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 NavigationPointerEventsContainer + * @flow + */ +'use strict'; + +const React = require('React'); +const NavigationAnimatedValueSubscription = require('NavigationAnimatedValueSubscription'); + +const invariant = require('fbjs/lib/invariant'); + +import type { + NavigationSceneRendererProps, +} from 'NavigationTypeDefinition'; + +type Props = NavigationSceneRendererProps; + +const MIN_POSITION_OFFSET = 0.01; + +/** + * Create a higher-order component that automatically computes the + * `pointerEvents` property for a component whenever navigation position + * changes. + */ +function create( + Component: ReactClass, +): ReactClass { + + class Container extends React.Component { + + _component: any; + _onComponentRef: (view: any) => void; + _onPositionChange: (data: {value: number}) => void; + _pointerEvents: string; + _positionListener: ?NavigationAnimatedValueSubscription; + + props: Props; + + constructor(props: Props, context: any) { + super(props, context); + this._pointerEvents = this._computePointerEvents(); + } + + componentWillMount(): void { + this._onPositionChange = this._onPositionChange.bind(this); + this._onComponentRef = this._onComponentRef.bind(this); + } + + componentDidMount(): void { + this._bindPosition(this.props); + } + + componentWillUnmount(): void { + this._positionListener && this._positionListener.remove(); + } + + componentWillReceiveProps(nextProps: Props): void { + this._bindPosition(nextProps); + } + + render(): ReactElement { + this._pointerEvents = this._computePointerEvents(); + return ( + + ); + } + + _onComponentRef(component: any): void { + this._component = component; + if (component) { + invariant( + typeof component.setNativeProps === 'function', + 'component must implement method `setNativeProps`', + ); + } + } + + _bindPosition(props: NavigationSceneRendererProps): void { + this._positionListener && this._positionListener.remove(); + this._positionListener = new NavigationAnimatedValueSubscription( + props.position, + this._onPositionChange, + ); + } + + _onPositionChange(): void { + if (this._component) { + const pointerEvents = this._computePointerEvents(); + if (this._pointerEvents !== pointerEvents) { + this._pointerEvents = pointerEvents; + this._component.setNativeProps({pointerEvents}); + } + } + } + + _computePointerEvents(): string { + const { + navigationState, + position, + scene, + } = this.props; + + if (scene.isStale || navigationState.index !== scene.index) { + // The scene isn't focused. + return 'none'; + } + + const offset = position.__getAnimatedValue() - navigationState.index; + if (Math.abs(offset) > MIN_POSITION_OFFSET) { + // The positon is still away from scene's index. + // Scene's children should not receive touches until the position + // is close enough to scene's index. + return 'box-only'; + } + + return 'auto'; + } + } + return Container; +} + +module.exports = { + create, +};