Jay Salvat e62471b7c3 Fix pop method to properly refresh the navigatorBar
Summary:
When using nativator.pop() the navigatorBar doesn't refresh properly if we change navigationBarHidden.

When navigator.pop() is called, the navbar doesn't show up, even avec navigationBarHidden set back to true. See between Statusbar and Movie poster in the example below.

![bad](https://cloud.githubusercontent.com/assets/55496/8683849/77effa30-2a75-11e5-9f32-9be6fa87efac.gif)

    // Not actually updating the indices yet until we get the native
    // `onNavigationComplete`.
    updatingAllIndicesAtOrBeyond: null,

https://github.com/facebook/react-native/blob/master/Libraries/Components/Navigation/NavigatorIOS.ios.js#L510-L512

The View need to be refreshed in pop method anyway.

    updatingAllIndicesAtOrBeyond: this.state.requestedTopOfStack - n,

Now, navbar appears properly.

![good](https://cloud.githubusercontent.com/assets/55496/8683907/be9d2714-2a75-11e5-8fda-653e68869076.gif)
Closes https://github.com/facebook/react-native/pull/1989

Reviewed By: svcscm

Differential Revision: D2798515

Pulled By: androidtrunkagent

fb-gh-sync-id: 9d554c9d88b87628488a8cebb7e3bbe4dd9c017d
2016-01-02 22:52:30 -08:00

680 lines
20 KiB
JavaScript

/**
* Copyright (c) 2015-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 NavigatorIOS
* @flow
*/
'use strict';
var EventEmitter = require('EventEmitter');
var Image = require('Image');
var NavigationContext = require('NavigationContext');
var RCTNavigatorManager = require('NativeModules').NavigatorManager;
var React = require('React');
var StaticContainer = require('StaticContainer.react');
var StyleSheet = require('StyleSheet');
var View = require('View');
var invariant = require('invariant');
var logError = require('logError');
var requireNativeComponent = require('requireNativeComponent');
var resolveAssetSource = require('resolveAssetSource');
var TRANSITIONER_REF = 'transitionerRef';
var PropTypes = React.PropTypes;
var __uid = 0;
function getuid() {
return __uid++;
}
var NavigatorTransitionerIOS = React.createClass({
requestSchedulingNavigation: function(cb) {
RCTNavigatorManager.requestSchedulingJavaScriptNavigation(
React.findNodeHandle(this),
logError,
cb
);
},
render: function() {
return (
<RCTNavigator {...this.props}/>
);
},
});
type Route = {
component: Function;
title: string;
passProps?: Object;
backButtonTitle?: string;
backButtonIcon?: Object;
leftButtonTitle?: string;
leftButtonIcon?: Object;
onLeftButtonPress?: Function;
rightButtonTitle?: string;
rightButtonIcon?: Object;
onRightButtonPress?: Function;
wrapperStyle?: any;
};
type State = {
idStack: Array<number>;
routeStack: Array<Route>;
requestedTopOfStack: number;
observedTopOfStack: number;
progress: number;
fromIndex: number;
toIndex: number;
makingNavigatorRequest: boolean;
updatingAllIndicesAtOrBeyond: ?number;
}
type Event = Object;
/**
* Think of `<NavigatorIOS>` as simply a component that renders an
* `RCTNavigator`, and moves the `RCTNavigator`'s `requestedTopOfStack` pointer
* forward and backward. The `RCTNavigator` interprets changes in
* `requestedTopOfStack` to be pushes and pops of children that are rendered.
* `<NavigatorIOS>` always ensures that whenever the `requestedTopOfStack`
* pointer is moved, that we've also rendered enough children so that the
* `RCTNavigator` can carry out the push/pop with those children.
* `<NavigatorIOS>` also removes children that will no longer be needed
* (after the pop of a child has been fully completed/animated out).
*/
/**
* NavigatorIOS wraps UIKit navigation and allows you to add back-swipe
* functionality across your app.
*
* > **NOTE**: This Component is not maintained by Facebook
* >
* > This component is under community responsibility.
* > If a pure JavaScript solution fits your needs you may try the `Navigator`
* > component instead.
*
* #### Routes
* A route is an object used to describe each page in the navigator. The first
* route is provided to NavigatorIOS as `initialRoute`:
*
* ```
* render: function() {
* return (
* <NavigatorIOS
* initialRoute={{
* component: MyView,
* title: 'My View Title',
* passProps: { myProp: 'foo' },
* }}
* />
* );
* },
* ```
*
* Now MyView will be rendered by the navigator. It will receive the route
* object in the `route` prop, a navigator, and all of the props specified in
* `passProps`.
*
* See the initialRoute propType for a complete definition of a route.
*
* #### Navigator
*
* A `navigator` is an object of navigation functions that a view can call. It
* is passed as a prop to any component rendered by NavigatorIOS.
*
* ```
* var MyView = React.createClass({
* _handleBackButtonPress: function() {
* this.props.navigator.pop();
* },
* _handleNextButtonPress: function() {
* this.props.navigator.push(nextRoute);
* },
* ...
* });
* ```
*
* A navigation object contains the following functions:
*
* - `push(route)` - Navigate forward to a new route
* - `pop()` - Go back one page
* - `popN(n)` - Go back N pages at once. When N=1, behavior matches `pop()`
* - `replace(route)` - Replace the route for the current page and immediately
* load the view for the new route
* - `replacePrevious(route)` - Replace the route/view for the previous page
* - `replacePreviousAndPop(route)` - Replaces the previous route/view and
* transitions back to it
* - `resetTo(route)` - Replaces the top item and popToTop
* - `popToRoute(route)` - Go back to the item for a particular route object
* - `popToTop()` - Go back to the top item
*
* Navigator functions are also available on the NavigatorIOS component:
*
* ```
* var MyView = React.createClass({
* _handleNavigationRequest: function() {
* this.refs.nav.push(otherRoute);
* },
* render: () => (
* <NavigatorIOS
* ref="nav"
* initialRoute={...}
* />
* ),
* });
* ```
*
*/
var NavigatorIOS = React.createClass({
propTypes: {
/**
* NavigatorIOS uses "route" objects to identify child views, their props,
* and navigation bar configuration. "push" and all the other navigation
* operations expect routes to be like this:
*/
initialRoute: PropTypes.shape({
/**
* The React Class to render for this route
*/
component: PropTypes.func.isRequired,
/**
* The title displayed in the nav bar and back button for this route
*/
title: PropTypes.string.isRequired,
/**
* Specify additional props passed to the component. NavigatorIOS will
* automatically provide "route" and "navigator" components
*/
passProps: PropTypes.object,
/**
* If set, the left header button image will appear with this source. Note
* that this doesn't apply for the header of the current view, but the
* ones of the views that are pushed afterward.
*/
backButtonIcon: Image.propTypes.source,
/**
* If set, the left header button will appear with this name. Note that
* this doesn't apply for the header of the current view, but the ones
* of the views that are pushed afterward.
*/
backButtonTitle: PropTypes.string,
/**
* If set, the left header button image will appear with this source
*/
leftButtonIcon: Image.propTypes.source,
/**
* If set, the left header button will appear with this name
*/
leftButtonTitle: PropTypes.string,
/**
* Called when the left header button is pressed
*/
onLeftButtonPress: PropTypes.func,
/**
* If set, the right header button image will appear with this source
*/
rightButtonIcon: Image.propTypes.source,
/**
* If set, the right header button will appear with this name
*/
rightButtonTitle: PropTypes.string,
/**
* Called when the right header button is pressed
*/
onRightButtonPress: PropTypes.func,
/**
* Styles for the navigation item containing the component
*/
wrapperStyle: View.propTypes.style,
}).isRequired,
/**
* A Boolean value that indicates whether the navigation bar is hidden
*/
navigationBarHidden: PropTypes.bool,
/**
* A Boolean value that indicates whether to hide the 1px hairline shadow
*/
shadowHidden: PropTypes.bool,
/**
* The default wrapper style for components in the navigator.
* A common use case is to set the backgroundColor for every page
*/
itemWrapperStyle: View.propTypes.style,
/**
* The color used for buttons in the navigation bar
*/
tintColor: PropTypes.string,
/**
* The background color of the navigation bar
*/
barTintColor: PropTypes.string,
/**
* The text color of the navigation bar title
*/
titleTextColor: PropTypes.string,
/**
* A Boolean value that indicates whether the navigation bar is translucent
*/
translucent: PropTypes.bool,
},
navigator: (undefined: ?Object),
navigationContext: new NavigationContext(),
componentWillMount: function() {
// Precompute a pack of callbacks that's frequently generated and passed to
// instances.
this.navigator = {
push: this.push,
pop: this.pop,
popN: this.popN,
replace: this.replace,
replacePrevious: this.replacePrevious,
replacePreviousAndPop: this.replacePreviousAndPop,
resetTo: this.resetTo,
popToRoute: this.popToRoute,
popToTop: this.popToTop,
navigationContext: this.navigationContext,
};
this._emitWillFocus(this.state.routeStack[this.state.observedTopOfStack]);
},
componentDidMount: function() {
this._emitDidFocus(this.state.routeStack[this.state.observedTopOfStack]);
},
componentWillUnmount: function() {
this.navigationContext.dispose();
this.navigationContext = new NavigationContext();
},
getDefaultProps: function(): Object {
return {
translucent: true,
};
},
getInitialState: function(): State {
return {
idStack: [getuid()],
routeStack: [this.props.initialRoute],
// The navigation index that we wish to push/pop to.
requestedTopOfStack: 0,
// The last index that native has sent confirmation of completed push/pop
// for. At this point, we can discard any views that are beyond the
// `requestedTopOfStack`. A value of `null` means we have not received
// any confirmation, ever. We may receive an `observedTopOfStack` without
// ever requesting it - native can instigate pops of its own with the
// backswipe gesture.
observedTopOfStack: 0,
progress: 1,
fromIndex: 0,
toIndex: 0,
// Whether or not we are making a navigator request to push/pop. (Used
// for performance optimization).
makingNavigatorRequest: false,
// Whether or not we are updating children of navigator and if so (not
// `null`) which index marks the beginning of all updates. Used for
// performance optimization.
updatingAllIndicesAtOrBeyond: 0,
};
},
_toFocusOnNavigationComplete: (undefined: any),
_handleFocusRequest: function(item: any) {
if (this.state.makingNavigatorRequest) {
this._toFocusOnNavigationComplete = item;
} else {
this._getFocusEmitter().emit('focus', item);
}
},
_focusEmitter: (undefined: ?EventEmitter),
_getFocusEmitter: function(): EventEmitter {
// Flow not yet tracking assignments to instance fields.
var focusEmitter = this._focusEmitter;
if (!focusEmitter) {
focusEmitter = new EventEmitter();
this._focusEmitter = focusEmitter;
}
return focusEmitter;
},
getChildContext: function(): {
onFocusRequested: Function;
focusEmitter: EventEmitter;
} {
return {
onFocusRequested: this._handleFocusRequest,
focusEmitter: this._getFocusEmitter(),
};
},
childContextTypes: {
onFocusRequested: React.PropTypes.func,
focusEmitter: React.PropTypes.instanceOf(EventEmitter),
},
_tryLockNavigator: function(cb: () => void) {
this.refs[TRANSITIONER_REF].requestSchedulingNavigation(
(acquiredLock) => acquiredLock && cb()
);
},
_handleNavigatorStackChanged: function(e: Event) {
var newObservedTopOfStack = e.nativeEvent.stackLength - 1;
this._emitDidFocus(this.state.routeStack[newObservedTopOfStack]);
invariant(
newObservedTopOfStack <= this.state.requestedTopOfStack,
'No navigator item should be pushed without JS knowing about it %s %s', newObservedTopOfStack, this.state.requestedTopOfStack
);
var wasWaitingForConfirmation =
this.state.requestedTopOfStack !== this.state.observedTopOfStack;
if (wasWaitingForConfirmation) {
invariant(
newObservedTopOfStack === this.state.requestedTopOfStack,
'If waiting for observedTopOfStack to reach requestedTopOfStack, ' +
'the only valid observedTopOfStack should be requestedTopOfStack.'
);
}
// Mark the most recent observation regardless of if we can lock the
// navigator. `observedTopOfStack` merely represents what we've observed
// and this first `setState` is only executed to update debugging
// overlays/navigation bar.
// Also reset progress, toIndex, and fromIndex as they might not end
// in the correct states for a two possible reasons:
// Progress isn't always 0 or 1 at the end, the system rounds
// If the Navigator is offscreen these values won't be updated
// TOOD: Revisit this decision when no longer relying on native navigator.
var nextState = {
observedTopOfStack: newObservedTopOfStack,
makingNavigatorRequest: false,
updatingAllIndicesAtOrBeyond: null,
progress: 1,
toIndex: newObservedTopOfStack,
fromIndex: newObservedTopOfStack,
};
this.setState(nextState, this._eliminateUnneededChildren);
},
_eliminateUnneededChildren: function() {
// Updating the indices that we're deleting and that's all. (Truth: Nothing
// even uses the indices in this case, but let's make this describe the
// truth anyways).
var updatingAllIndicesAtOrBeyond =
this.state.routeStack.length > this.state.observedTopOfStack + 1 ?
this.state.observedTopOfStack + 1 :
null;
this.setState({
idStack: this.state.idStack.slice(0, this.state.observedTopOfStack + 1),
routeStack: this.state.routeStack.slice(0, this.state.observedTopOfStack + 1),
// Now we rerequest the top of stack that we observed.
requestedTopOfStack: this.state.observedTopOfStack,
makingNavigatorRequest: true,
updatingAllIndicesAtOrBeyond: updatingAllIndicesAtOrBeyond,
});
},
_emitDidFocus: function(route: Route) {
this.navigationContext.emit('didfocus', {route: route});
},
_emitWillFocus: function(route: Route) {
this.navigationContext.emit('willfocus', {route: route});
},
push: function(route: Route) {
invariant(!!route, 'Must supply route to push');
// Make sure all previous requests are caught up first. Otherwise reject.
if (this.state.requestedTopOfStack === this.state.observedTopOfStack) {
this._tryLockNavigator(() => {
this._emitWillFocus(route);
var nextStack = this.state.routeStack.concat([route]);
var nextIDStack = this.state.idStack.concat([getuid()]);
this.setState({
// We have to make sure that we've also supplied enough views to
// satisfy our request to adjust the `requestedTopOfStack`.
idStack: nextIDStack,
routeStack: nextStack,
requestedTopOfStack: nextStack.length - 1,
makingNavigatorRequest: true,
updatingAllIndicesAtOrBeyond: nextStack.length - 1,
});
});
}
},
popN: function(n: number) {
if (n === 0) {
return;
}
// Make sure all previous requests are caught up first. Otherwise reject.
if (this.state.requestedTopOfStack === this.state.observedTopOfStack) {
if (this.state.requestedTopOfStack > 0) {
this._tryLockNavigator(() => {
var newRequestedTopOfStack = this.state.requestedTopOfStack - n;
invariant(newRequestedTopOfStack >= 0, 'Cannot pop below 0');
this._emitWillFocus(this.state.routeStack[newRequestedTopOfStack]);
this.setState({
requestedTopOfStack: newRequestedTopOfStack,
makingNavigatorRequest: true,
updatingAllIndicesAtOrBeyond: this.state.requestedTopOfStack - n,
});
});
}
}
},
pop: function() {
this.popN(1);
},
/**
* Replace a route in the navigation stack.
*
* `index` specifies the route in the stack that should be replaced.
* If it's negative, it counts from the back.
*/
replaceAtIndex: function(route: Route, index: number) {
invariant(!!route, 'Must supply route to replace');
if (index < 0) {
index += this.state.routeStack.length;
}
if (this.state.routeStack.length <= index) {
return;
}
// I don't believe we need to lock for a replace since there's no
// navigation actually happening
var nextIDStack = this.state.idStack.slice();
var nextRouteStack = this.state.routeStack.slice();
nextIDStack[index] = getuid();
nextRouteStack[index] = route;
this.setState({
idStack: nextIDStack,
routeStack: nextRouteStack,
makingNavigatorRequest: false,
updatingAllIndicesAtOrBeyond: index,
});
this._emitWillFocus(route);
this._emitDidFocus(route);
},
/**
* Replaces the top of the navigation stack.
*/
replace: function(route: Route) {
this.replaceAtIndex(route, -1);
},
/**
* Replace the current route's parent.
*/
replacePrevious: function(route: Route) {
this.replaceAtIndex(route, -2);
},
popToTop: function() {
this.popToRoute(this.state.routeStack[0]);
},
popToRoute: function(route: Route) {
var indexOfRoute = this.state.routeStack.indexOf(route);
invariant(
indexOfRoute !== -1,
'Calling pop to route for a route that doesn\'t exist!'
);
var numToPop = this.state.routeStack.length - indexOfRoute - 1;
this.popN(numToPop);
},
replacePreviousAndPop: function(route: Route) {
// Make sure all previous requests are caught up first. Otherwise reject.
if (this.state.requestedTopOfStack !== this.state.observedTopOfStack) {
return;
}
if (this.state.routeStack.length < 2) {
return;
}
this._tryLockNavigator(() => {
this.replacePrevious(route);
this.setState({
requestedTopOfStack: this.state.requestedTopOfStack - 1,
makingNavigatorRequest: true,
});
});
},
resetTo: function(route: Route) {
invariant(!!route, 'Must supply route to push');
// Make sure all previous requests are caught up first. Otherwise reject.
if (this.state.requestedTopOfStack !== this.state.observedTopOfStack) {
return;
}
this.replaceAtIndex(route, 0);
this.popToRoute(route);
},
handleNavigationComplete: function(e: Event) {
if (this._toFocusOnNavigationComplete) {
this._getFocusEmitter().emit('focus', this._toFocusOnNavigationComplete);
this._toFocusOnNavigationComplete = null;
}
this._handleNavigatorStackChanged(e);
},
_routeToStackItem: function(route: Route, i: number) {
var {component, wrapperStyle, passProps, ...route} = route;
var {itemWrapperStyle, ...props} = this.props;
var shouldUpdateChild =
this.state.updatingAllIndicesAtOrBeyond &&
this.state.updatingAllIndicesAtOrBeyond >= i;
var Component = component;
return (
<StaticContainer key={'nav' + i} shouldUpdate={shouldUpdateChild}>
<RCTNavigatorItem
{...route}
{...props}
style={[
styles.stackItem,
itemWrapperStyle,
wrapperStyle
]}>
<Component
navigator={this.navigator}
route={route}
{...passProps}
/>
</RCTNavigatorItem>
</StaticContainer>
);
},
renderNavigationStackItems: function() {
var shouldRecurseToNavigator =
this.state.makingNavigatorRequest ||
this.state.updatingAllIndicesAtOrBeyond !== null;
// If not recursing update to navigator at all, may as well avoid
// computation of navigator children.
var items = shouldRecurseToNavigator ?
this.state.routeStack.map(this._routeToStackItem) : null;
return (
<StaticContainer shouldUpdate={shouldRecurseToNavigator}>
<NavigatorTransitionerIOS
ref={TRANSITIONER_REF}
style={styles.transitioner}
vertical={this.props.vertical}
requestedTopOfStack={this.state.requestedTopOfStack}
onNavigationComplete={this.handleNavigationComplete}>
{items}
</NavigatorTransitionerIOS>
</StaticContainer>
);
},
render: function() {
return (
<View style={this.props.style}>
{this.renderNavigationStackItems()}
</View>
);
},
});
var styles = StyleSheet.create({
stackItem: {
backgroundColor: 'white',
overflow: 'hidden',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
},
transitioner: {
flex: 1,
},
});
var RCTNavigator = requireNativeComponent('RCTNavigator');
var RCTNavigatorItem = requireNativeComponent('RCTNavItem');
module.exports = NavigatorIOS;