Refactor <NavigationHeader /> API.

Summary:- All the public sub component renderers should implement the interface
  NavigationSceneRenderer, which will help to reuse renderer or
  replace renders for different composition.

- Perf improvement. <NavigationHeader /> is rendering way more
  sub component than necessary, we shall fix that.

- No UI or behavior change.

Reviewed By: ericvicenti

Differential Revision: D3091442

fb-gh-sync-id: fba5f7ce74597fa6036b5b216c02b06052801983
shipit-source-id: fba5f7ce74597fa6036b5b216c02b06052801983
This commit is contained in:
Hedger Wang
2016-03-24 15:48:27 -07:00
committed by Facebook Github Bot 9
parent 433fb336af
commit 62e80a600e
7 changed files with 324 additions and 168 deletions

View File

@@ -58,10 +58,11 @@ const NavigationBasicReducer = NavigationReducer.StackReducer({
class NavigationAnimatedExample extends React.Component {
componentWillMount() {
this._renderNavigation = this._renderNavigation.bind(this);
this._renderCard = this._renderCard.bind(this);
this._renderScene = this._renderScene.bind(this);
this._renderHeader = this._renderHeader.bind(this);
this._renderNavigation = this._renderNavigation.bind(this);
this._renderScene = this._renderScene.bind(this);
this._renderTitleComponent = this._renderTitleComponent.bind(this);
}
render() {
return (
@@ -99,14 +100,20 @@ class NavigationAnimatedExample extends React.Component {
_renderHeader(/*NavigationSceneRendererProps*/ props) {
return (
<NavigationHeader
navigationProps={props}
renderTitleComponent={(navigationProps, scene) => {
return <NavigationHeader.Title>{scene.navigationState.key}</NavigationHeader.Title>;
}}
{...props}
renderTitleComponent={this._renderTitleComponent}
/>
);
}
_renderTitleComponent(/*NavigationSceneRendererProps*/ props) {
return (
<NavigationHeader.Title>
{props.scene.navigationState.key}
</NavigationHeader.Title>
);
}
_renderCard(/*NavigationSceneRendererProps*/ props) {
return (
<NavigationCard

View File

@@ -1,4 +1,11 @@
/**
* 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.
*
* The examples provided by Facebook are for non-commercial testing and
* evaluation purposes only.
*
@@ -139,7 +146,7 @@ const ExampleAppReducer = NavigationReducer.TabsReducer({
],
});
function stateTypeTitleMap(pageState) {
function stateTypeTitleMap(pageState: any) {
switch (pageState.type) {
case 'ProfilePage':
return 'Profile Page';
@@ -177,14 +184,20 @@ class ExampleTabScreen extends React.Component {
_renderHeader(props: NavigationSceneRendererProps) {
return (
<NavigationHeader
navigationProps={props}
renderTitleComponent={(navigationProps, scene) => {
return <NavigationHeader.Title>{stateTypeTitleMap(scene.navigationState)}</NavigationHeader.Title>;
}}
{...props}
renderTitleComponent={this._renderTitleComponent}
/>
);
}
_renderTitleComponent(props: NavigationSceneRendererProps) {
return (
<NavigationHeader.Title>
{stateTypeTitleMap(props.scene.navigationState)}
</NavigationHeader.Title>
);
}
_renderScene(props: NavigationSceneRendererProps) {
const {onNavigate} = props;
return (

View File

@@ -1,4 +1,11 @@
/**
* 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.
*
* The examples provided by Facebook are for non-commercial testing and
* evaluation purposes only.
*
@@ -77,10 +84,12 @@ class UIExplorerApp extends React.Component {
_renderOverlay: Function;
_renderScene: Function;
_renderCard: Function;
_renderTitleComponent: Function;
componentWillMount() {
this._renderNavigation = this._renderNavigation.bind(this);
this._renderOverlay = this._renderOverlay.bind(this);
this._renderScene = this._renderScene.bind(this);
this._renderTitleComponent = this._renderTitleComponent.bind(this);
}
render() {
return (
@@ -121,15 +130,20 @@ class UIExplorerApp extends React.Component {
_renderOverlay(props: NavigationSceneRendererProps): ReactElement {
return (
<NavigationHeader
key={'header_' + props.scene.navigationState.key}
navigationProps={props}
renderTitleComponent={(navigationProps, scene) => {
return <NavigationHeader.Title>{UIExplorerStateTitleMap(scene.navigationState)}</NavigationHeader.Title>;
}}
{...props}
renderTitleComponent={this._renderTitleComponent}
/>
);
}
_renderTitleComponent(props: NavigationSceneRendererProps): ReactElement {
return (
<NavigationHeader.Title>
{UIExplorerStateTitleMap(props.scene.navigationState)}
</NavigationHeader.Title>
);
}
_renderScene(props: NavigationSceneRendererProps): ?ReactElement {
const state = props.scene.navigationState;
if (state.key === 'AppList') {

View File

@@ -1,5 +1,10 @@
/**
* Copyright (c) 2015, Facebook, Inc. All rights reserved.
* 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
@@ -32,6 +37,8 @@ const NavigationContainer = require('NavigationContainer');
const NavigationHeaderTitle = require('NavigationHeaderTitle');
const NavigationHeaderBackButton = require('NavigationHeaderBackButton');
const NavigationPropTypes = require('NavigationPropTypes');
const NavigationHeaderStyleInterpolator = require('NavigationHeaderStyleInterpolator');
const ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin');
const {
Animated,
@@ -41,210 +48,197 @@ const {
} = React;
import type {
NavigationSceneRenderer,
NavigationSceneRendererProps,
NavigationScene,
NavigationStyleInterpolator,
} from 'NavigationTypeDefinition';
type Renderer = (props: NavigationSceneRendererProps, scene: NavigationScene) => ?ReactElement;
type DefaultProps = {
renderTitleComponent: Renderer;
renderLeftComponent: Renderer;
renderLeftComponent: NavigationSceneRenderer,
renderRightComponent: NavigationSceneRenderer,
renderTitleComponent: NavigationSceneRenderer,
};
type Props = {
navigationProps: NavigationSceneRendererProps;
renderTitleComponent: Renderer;
renderLeftComponent: Renderer;
renderRightComponent: Renderer;
type Props = NavigationSceneRendererProps & {
renderLeftComponent: NavigationSceneRenderer,
renderRightComponent: NavigationSceneRenderer,
renderTitleComponent: NavigationSceneRenderer,
style?: any;
}
};
type SubViewName = 'left' | 'title' | 'right';
const APPBAR_HEIGHT = Platform.OS === 'ios' ? 44 : 56;
const STATUSBAR_HEIGHT = Platform.OS === 'ios' ? 20 : 0;
const {PropTypes} = React;
class NavigationHeader extends React.Component<DefaultProps, Props, any> {
props: Props;
class NavigationHeader extends React.Component<DefaultProps, Props, void> {
static defaultProps = {
renderTitleComponent: (props, scene) => {
const pageState = scene.navigationState;
return <NavigationHeaderTitle>{pageState.title ? pageState.title : ''}</NavigationHeaderTitle>;
renderTitleComponent: (props: NavigationSceneRendererProps) => {
const {navigationState} = props;
const title = String(navigationState.title || '');
return <NavigationHeaderTitle>{title}</NavigationHeaderTitle>;
},
renderLeftComponent: (props: NavigationSceneRendererProps) => {
return props.scene.index > 0 ? <NavigationHeaderBackButton /> : null;
},
renderRightComponent: (props: NavigationSceneRendererProps) => {
return null;
},
renderLeftComponent: (props, scene) => scene.index !== 0 ? <NavigationHeaderBackButton /> : null
};
static propTypes = {
navigationProps: React.PropTypes.shape(NavigationPropTypes.SceneRenderer).isRequired,
renderTitleComponent: React.PropTypes.func,
renderLeftComponent: React.PropTypes.func,
renderRightComponent: React.PropTypes.func,
...NavigationPropTypes.SceneRenderer,
renderLeftComponent: PropTypes.func,
renderRightComponent: PropTypes.func,
renderTitleComponent: PropTypes.func,
style: View.propTypes.style,
};
_renderLeftComponent(scene: NavigationScene) {
const {
renderLeftComponent,
navigationProps,
} = this.props;
if (renderLeftComponent) {
const {
index,
navigationState,
} = scene;
return (
<Animated.View
pointerEvents={navigationProps.navigationState.index === index ? 'auto' : 'none'}
key={navigationState.key}
style={[
styles.left,
{
opacity: navigationProps.position.interpolate({
inputRange: [ index - 1, index, index + 1 ],
outputRange: [ 0, 1, 0 ],
})
}
]}
>
{renderLeftComponent(navigationProps, scene)}
</Animated.View>
);
}
return null;
shouldComponentUpdate(nextProps: Props, nextState: any): boolean {
return ReactComponentWithPureRenderMixin.shouldComponentUpdate.call(
this,
nextProps,
nextState
);
}
_renderRightComponent(scene: NavigationScene) {
const {
renderRightComponent,
navigationProps,
} = this.props;
render(): ReactElement {
const { scenes, style } = this.props;
if (renderRightComponent) {
const {
index,
navigationState,
} = scene;
return (
<Animated.View
pointerEvents={navigationProps.navigationState.index === index ? 'auto' : 'none'}
key={navigationState.key}
style={[
styles.right,
{
opacity: navigationProps.position.interpolate({
inputRange: [ index - 1, index, index + 1 ],
outputRange: [ 0, 1, 0 ],
})
}
]}
>
{renderRightComponent(navigationProps, scene)}
</Animated.View>
);
}
return null;
}
_renderTitleComponent(scene: NavigationScene) {
const {
renderTitleComponent,
navigationProps,
} = this.props;
if (renderTitleComponent) {
const {
index,
navigationState,
} = scene;
return (
<Animated.View
pointerEvents={navigationProps.navigationState.index === index ? 'auto' : 'none'}
key={navigationState.key}
style={[
styles.title,
{
opacity: navigationProps.position.interpolate({
inputRange: [ index - 1, index, index + 1 ],
outputRange: [ 0, 1, 0 ],
}),
transform: [
{
translateX: navigationProps.position.interpolate({
inputRange: [ index - 1, index + 1 ],
outputRange: [ 200, -200 ],
}),
}
],
}
]}
>
{renderTitleComponent(navigationProps, scene)}
</Animated.View>
);
}
return null;
}
render() {
const { scenes } = this.props.navigationProps;
const scenesProps = scenes.map(scene => {
const props = NavigationPropTypes.extractSceneRendererProps(this.props);
props.scene = scene;
return props;
});
return (
<View style={[ styles.appbar, this.props.style ]}>
{scenes.map(this._renderLeftComponent, this)}
{scenes.map(this._renderTitleComponent, this)}
{scenes.map(this._renderRightComponent, this)}
<View style={[ styles.appbar, style ]}>
{scenesProps.map(this._renderLeft, this)}
{scenesProps.map(this._renderTitle, this)}
{scenesProps.map(this._renderRight, this)}
</View>
);
}
_renderLeft(props: NavigationSceneRendererProps): ?ReactElement {
return this._renderSubView(
props,
'left',
this.props.renderLeftComponent,
NavigationHeaderStyleInterpolator.forLeft,
);
}
_renderTitle(props: NavigationSceneRendererProps): ?ReactElement {
return this._renderSubView(
props,
'title',
this.props.renderTitleComponent,
NavigationHeaderStyleInterpolator.forCenter,
);
}
_renderRight(props: NavigationSceneRendererProps): ?ReactElement {
return this._renderSubView(
props,
'right',
this.props.renderRightComponent,
NavigationHeaderStyleInterpolator.forRight,
);
}
_renderSubView(
props: NavigationSceneRendererProps,
name: SubViewName,
renderer: NavigationSceneRenderer,
styleInterpolator: NavigationStyleInterpolator,
): ?ReactElement {
const {
scene,
navigationState,
} = props;
const {
index,
isStale,
key,
} = scene;
const offset = navigationState.index - index;
if (Math.abs(offset) > 2) {
// Scene is far away from the active scene. Hides it to avoid unnecessary
// rendering.
return null;
}
const subView = renderer(props);
if (subView === null) {
return null;
}
const pointerEvents = offset !== 0 || isStale ? 'none' : 'auto';
return (
<Animated.View
pointerEvents={pointerEvents}
key={name + '_' + key}
style={[
styles[name],
styleInterpolator(props),
]}>
{subView}
</Animated.View>
);
}
}
const styles = StyleSheet.create({
appbar: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-start',
backgroundColor: Platform.OS === 'ios' ? '#EFEFF2' : '#FFF',
borderBottomWidth: Platform.OS === 'ios' ? StyleSheet.hairlineWidth : 0,
borderBottomColor: 'rgba(0, 0, 0, .15)',
height: APPBAR_HEIGHT + STATUSBAR_HEIGHT,
marginBottom: 16, // This is needed for elevation shadow
borderBottomWidth: Platform.OS === 'ios' ? StyleSheet.hairlineWidth : 0,
elevation: 2,
flexDirection: 'row',
height: APPBAR_HEIGHT + STATUSBAR_HEIGHT,
justifyContent: 'flex-start',
left: 0,
marginBottom: 16, // This is needed for elevation shadow
position: 'absolute',
right: 0,
top: 0,
},
title: {
position: 'absolute',
top: 0,
bottom: 0,
left: APPBAR_HEIGHT,
right: APPBAR_HEIGHT,
marginTop: STATUSBAR_HEIGHT,
position: 'absolute',
right: APPBAR_HEIGHT,
top: 0,
},
left: {
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
marginTop: STATUSBAR_HEIGHT,
position: 'absolute',
top: 0,
},
right: {
position: 'absolute',
top: 0,
bottom: 0,
right: 0,
marginTop: STATUSBAR_HEIGHT,
}
position: 'absolute',
right: 0,
top: 0,
},
});
NavigationHeader = NavigationContainer.create(NavigationHeader);

View File

@@ -0,0 +1,96 @@
/**
* 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 NavigationHeaderStyleInterpolator
* @flow
*/
'use strict';
import type {
NavigationSceneRendererProps,
} from 'NavigationTypeDefinition';
/**
* Utility that builds the style for the navigation header.
*
* +-------------+-------------+-------------+
* | | | |
* | Left | Title | Right |
* | Component | Component | Component |
* | | | |
* +-------------+-------------+-------------+
*/
function forLeft(props: NavigationSceneRendererProps): Object {
const {position, scene} = props;
const {index} = scene;
return {
opacity: position.interpolate({
inputRange: [ index - 1, index, index + 1 ],
outputRange: [ 0, 1, 0 ],
}),
};
}
function forCenter(props: NavigationSceneRendererProps): Object {
const {position, scene} = props;
const {index} = scene;
return {
opacity:position.interpolate({
inputRange: [ index - 1, index, index + 1 ],
outputRange: [ 0, 1, 0 ],
}),
transform: [
{
translateX: position.interpolate({
inputRange: [ index - 1, index + 1 ],
outputRange: [ 200, -200 ],
}),
}
],
};
}
function forRight(props: NavigationSceneRendererProps): Object {
const {position, scene} = props;
const {index} = scene;
return {
opacity: position.interpolate({
inputRange: [ index - 1, index, index + 1 ],
outputRange: [ 0, 1, 0 ],
}),
};
}
module.exports = {
forCenter,
forLeft,
forRight,
};

View File

@@ -11,6 +11,10 @@
*/
'use strict';
import type {
NavigationSceneRendererProps,
} from 'NavigationTypeDefinition';
/**
* React component PropTypes Definitions. Consider using this as a supplementary
* measure with `NavigationTypeDefinition`. This helps to capture the propType
@@ -55,6 +59,7 @@ const layout = PropTypes.shape({
const scene = PropTypes.shape({
index: PropTypes.number.isRequired,
isStale: PropTypes.bool.isRequired,
key: PropTypes.string.isRequired,
navigationState,
});
@@ -84,8 +89,30 @@ const panHandlers = PropTypes.shape({
onStartShouldSetResponderCapture: PropTypes.func.isRequired,
});
/**
* Helper function that extracts the props needed for scene renderer.
*/
function extractSceneRendererProps(
props: NavigationSceneRendererProps,
): NavigationSceneRendererProps {
return {
layout: props.layout,
navigationState: props.navigationState,
onNavigate: props.onNavigate,
position: props.position,
scene: props.scene,
scenes: props.scenes,
};
}
module.exports = {
// helpers
extractSceneRendererProps,
// Bundled propTypes.
SceneRenderer,
// propTypes
action,
navigationParentState,
navigationState,

View File

@@ -107,3 +107,8 @@ export type NavigationReducer = (
export type NavigationSceneRenderer = (
props: NavigationSceneRendererProps,
) => ?ReactElement;
export type NavigationStyleInterpolator = (
props: NavigationSceneRendererProps,
) => Object;