[ReactNative] Fix Navigator scene hiding logic

Summary:
Scenes with 0 opacity are being rendered on top of other scenes with full absolute positioning. On iOS this is fine, because the platform will not send touch events to a view with no opacity. On Android is poses a problem because the view on top, even with no opacity, is catching the touch events for the presented scene below.

This change enhances the scene enabling and hiding logic, has better naming, and improves the documentation of it.

@public

Test Plan: Tested transitions and gestures in slow motion on iOS and Android
This commit is contained in:
Eric Vicenti 2015-05-04 02:40:45 -07:00
parent c5a6ec5b53
commit 548a0a6a4f

View File

@ -28,6 +28,7 @@
var AnimationsDebugModule = require('NativeModules').AnimationsDebugModule;
var BackAndroid = require('BackAndroid');
var Dimensions = require('Dimensions');
var InteractionMixin = require('InteractionMixin');
var NavigatorBreadcrumbNavigationBar = require('NavigatorBreadcrumbNavigationBar');
var NavigatorInterceptor = require('NavigatorInterceptor');
@ -39,6 +40,7 @@ var Platform = require('Platform');
var React = require('React');
var StaticContainer = require('StaticContainer.react');
var StyleSheet = require('StyleSheet');
var StyleSheetRegistry = require('StyleSheetRegistry');
var Subscribable = require('Subscribable');
var TimerMixin = require('react-timer-mixin');
var View = require('View');
@ -52,13 +54,37 @@ var rebound = require('rebound');
var PropTypes = React.PropTypes;
var OFF_SCREEN = {style: {opacity: 0}};
var SCREEN_WIDTH = Dimensions.get('window').width;
var SCENE_DISABLED_NATIVE_PROPS = {
style: {
left: SCREEN_WIDTH,
opacity: 0,
},
};
var __uid = 0;
function getuid() {
return __uid++;
}
function resolveStyle(styles) {
// The styles for a scene are a prop in the style format, which can be an object,
// a number (which refers to the StyleSheetRegistry), or an arry of objects/numbers.
// This function resolves the actual style values so we can call setNativeProps with
// matching styles.
var resolvedStyle = {};
if (!Array.isArray(styles)) {
styles = [styles];
}
styles.forEach((style) => {
if (typeof style === 'number') {
style = StyleSheetRegistry.getStyleByID(style);
}
resolvedStyle = merge(resolvedStyle, style);
});
return resolvedStyle;
}
// styles moved to the top of the file so getDefaultProps can refer to it
var styles = StyleSheet.create({
container: {
@ -72,7 +98,7 @@ var styles = StyleSheet.create({
bottom: 0,
top: 0,
},
currentScene: {
baseScene: {
position: 'absolute',
overflow: 'hidden',
left: 0,
@ -80,11 +106,8 @@ var styles = StyleSheet.create({
bottom: 0,
top: 0,
},
futureScene: {
overflow: 'hidden',
position: 'absolute',
left: 0,
opacity: 0,
disabledScene: {
left: SCREEN_WIDTH,
},
transitioner: {
flex: 1,
@ -571,10 +594,12 @@ var Navigator = React.createClass({
},
/**
* This happens at the end of a transition started by transitionTo
* This happens at the end of a transition started by transitionTo, and when the spring catches up to a pending gesture
*/
_completeTransition: function() {
if (this.spring.getCurrentValue() !== 1) {
// The spring has finished catching up to a gesture in progress. Remove the pending progress
// and we will be in a normal activeGesture state
if (this.state.pendingGestureProgress) {
this.state.pendingGestureProgress = null;
}
@ -599,11 +624,16 @@ var Navigator = React.createClass({
this._interactionHandle = null;
}
if (this.state.pendingGestureProgress) {
// A transition completed, but there is already another gesture happening.
// Enable the scene and set the spring to catch up with the new gesture
var gestureToIndex = this.state.presentedIndex + this._deltaForGestureAction(this.state.activeGesture);
this._enableScene(gestureToIndex);
this.spring.setEndValue(this.state.pendingGestureProgress);
return;
}
if (this.state.transitionQueue.length) {
var queuedTransition = this.state.transitionQueue.shift();
this._enableScene(queuedTransition.destIndex);
this._transitionTo(
queuedTransition.destIndex,
queuedTransition.velocity,
@ -644,21 +674,44 @@ var Navigator = React.createClass({
},
/**
* Does not delete the scenes - merely hides them.
* Hides scenes that we are not currently on or transitioning from
*/
_hideScenes: function() {
for (var i = 0; i < this.state.routeStack.length; i++) {
// This gets called when we detach a gesture, so there will not be a
// current gesture, but there might be a transition in progress
if (i === this.state.presentedIndex || i === this.state.transitionFromIndex) {
continue;
}
var sceneRef = 'scene_' + i;
this.refs[sceneRef] &&
this.refs['scene_' + i].setNativeProps(OFF_SCREEN);
this._disableScene(i);
}
},
/**
* Push a scene off the screen, so that opacity:0 scenes will not block touches sent to the presented scenes
*/
_disableScene: function(sceneIndex) {
this.refs['scene_' + sceneIndex] &&
this.refs['scene_' + sceneIndex].setNativeProps(SCENE_DISABLED_NATIVE_PROPS);
},
/**
* Put the scene back into the state as defined by props.sceneStyle, so transitions can happen normally
*/
_enableScene: function(sceneIndex) {
// First, determine what the defined styles are for scenes in this navigator
var sceneStyle = resolveStyle(this.props.sceneStyle);
// Then restore the left value for this scene
var enabledSceneNativeProps = {
left: sceneStyle.left,
};
if (sceneIndex !== this.state.transitionFromIndex) {
// If we are not in a transition from this index, make sure opacity is 0
// to prevent the enabled scene from flashing over the presented scene
enabledSceneNativeProps.opacity = 0;
}
this.refs['scene_' + sceneIndex] &&
this.refs['scene_' + sceneIndex].setNativeProps(enabledSceneNativeProps);
},
_onAnimationStart: function() {
var fromIndex = this.state.presentedIndex;
var toIndex = this.state.presentedIndex;
@ -669,7 +722,6 @@ var Navigator = React.createClass({
}
this._setRenderSceneToHarwareTextureAndroid(fromIndex, true);
this._setRenderSceneToHarwareTextureAndroid(toIndex, true);
var navBar = this._navBar;
if (navBar && navBar.onAnimationStart) {
navBar.onAnimationStart(fromIndex, toIndex);
@ -801,6 +853,8 @@ var Navigator = React.createClass({
_attachGesture: function(gestureId) {
this.state.activeGesture = gestureId;
var gesturingToIndex = this.state.presentedIndex + this._deltaForGestureAction(this.state.activeGesture);
this._enableScene(gesturingToIndex);
},
_detachGesture: function() {
@ -831,6 +885,7 @@ var Navigator = React.createClass({
(gesture.fullDistance - gestureDetectMovement);
if (nextProgress < 0 && gesture.isDetachable) {
this._detachGesture();
this.spring.setCurrentValue(0);
}
if (this._doesGestureOverswipe(this.state.activeGesture)) {
var frictionConstant = gesture.overswipe.frictionConstant;
@ -945,6 +1000,7 @@ var Navigator = React.createClass({
_jumpN: function(n) {
var destIndex = this._getDestIndexWithinBounds(n);
var requestTransitionAndResetUpdatingRange = () => {
this._enableScene(destIndex);
this._transitionTo(destIndex);
this._resetUpdatingRange();
};
@ -978,12 +1034,14 @@ var Navigator = React.createClass({
var activeIDStack = this.state.idStack.slice(0, activeLength);
var activeAnimationConfigStack = this.state.sceneConfigStack.slice(0, activeLength);
var nextStack = activeStack.concat([route]);
var destIndex = nextStack.length - 1;
var nextIDStack = activeIDStack.concat([getuid()]);
var nextAnimationConfigStack = activeAnimationConfigStack.concat([
this.props.configureScene(route),
]);
var requestTransitionAndResetUpdatingRange = () => {
this._transitionTo(nextStack.length - 1);
this._enableScene(destIndex);
this._transitionTo(destIndex);
this._resetUpdatingRange();
};
this.setState({
@ -1004,6 +1062,7 @@ var Navigator = React.createClass({
'Cannot pop below zero'
);
var popIndex = this.state.presentedIndex - n;
this._enableScene(popIndex);
this._transitionTo(
popIndex,
null, // default velocity
@ -1211,8 +1270,10 @@ var Navigator = React.createClass({
route,
sceneNavigatorContext
);
var initialSceneStyle = i === this.state.presentedIndex ?
styles.currentScene : styles.futureScene;
var disabledSceneStyle = null;
if (i !== this.state.presentedIndex) {
disabledSceneStyle = styles.disabledScene;
}
return (
<View
key={this.state.idStack[i]}
@ -1220,7 +1281,7 @@ var Navigator = React.createClass({
onStartShouldSetResponderCapture={() => {
return i !== this.state.presentedIndex;
}}
style={[initialSceneStyle, this.props.sceneStyle]}>
style={[styles.baseScene, this.props.sceneStyle, disabledSceneStyle]}>
{React.cloneElement(child, {
ref: this._handleItemRef.bind(null, this.state.idStack[i]),
})}