Refactor header, tintColor support, redesign iOS back button (#109)
@@ -57,6 +57,7 @@ const scene = PropTypes.shape({
|
||||
const SceneRendererProps = {
|
||||
layout: layout.isRequired,
|
||||
navigationState: navigationState.isRequired,
|
||||
navigation: PropTypes.object,
|
||||
position: animatedValue.isRequired,
|
||||
progress: animatedValue.isRequired,
|
||||
scene: scene.isRequired,
|
||||
@@ -94,6 +95,7 @@ function extractSceneRendererProps(
|
||||
position: props.position,
|
||||
progress: props.progress,
|
||||
scene: props.scene,
|
||||
navigation: props.navigation,
|
||||
scenes: props.scenes,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -381,6 +381,7 @@ export type NavigationTransitionProps = {
|
||||
// is the index of the scene
|
||||
scene: NavigationScene,
|
||||
index: number,
|
||||
navigation: NavigationScreenProp<NavigationRoute, NavigationAction>,
|
||||
|
||||
// The gesture distance for `horizontal` and `vertical` transitions
|
||||
gestureResponseDistance?: ?number,
|
||||
|
||||
@@ -21,6 +21,7 @@ import type {
|
||||
NavigationAction,
|
||||
NavigationScreenProp,
|
||||
NavigationState,
|
||||
NavigationScene,
|
||||
NavigationRoute,
|
||||
NavigationSceneRenderer,
|
||||
NavigationSceneRendererProps,
|
||||
@@ -158,7 +159,7 @@ class CardStack extends React.Component<DefaultProps, Props, void> {
|
||||
return (
|
||||
<Transitioner
|
||||
configureTransition={this._configureTransition}
|
||||
navigationState={this.props.navigation.state}
|
||||
navigation={this.props.navigation}
|
||||
render={this._render}
|
||||
style={this.props.style}
|
||||
/>
|
||||
@@ -191,49 +192,40 @@ class CardStack extends React.Component<DefaultProps, Props, void> {
|
||||
return transitionSpec;
|
||||
}
|
||||
|
||||
_renderHeader(props: NavigationTransitionProps, headerMode: HeaderMode): ?React.Element<*> {
|
||||
const navigation = this._getChildNavigation(props.scene);
|
||||
const header = this.props.router.getScreenConfig(navigation, 'header') || {};
|
||||
_renderHeader(
|
||||
transitionProps: NavigationTransitionProps,
|
||||
headerMode: HeaderMode
|
||||
): ?React.Element<*> {
|
||||
const headerConfig = this.props.router.getScreenConfig(
|
||||
transitionProps.navigation,
|
||||
'header'
|
||||
) || {};
|
||||
|
||||
return (
|
||||
<this.props.headerComponent
|
||||
{...props}
|
||||
style={header.style}
|
||||
{...transitionProps}
|
||||
router={this.props.router}
|
||||
style={headerConfig.style}
|
||||
mode={headerMode}
|
||||
onNavigateBack={() => this.props.navigation.goBack(null)}
|
||||
renderLeftComponent={(props) => {
|
||||
const navigation = this._getChildNavigation(props.scene);
|
||||
const header = this.props.router.getScreenConfig(navigation, 'header');
|
||||
if (header && header.left) {
|
||||
return header.left;
|
||||
}
|
||||
const { renderLeftComponent } = this.props.headerComponent.defaultProps || {};
|
||||
if (typeof renderLeftComponent === 'function') {
|
||||
return renderLeftComponent(props);
|
||||
}
|
||||
return null;
|
||||
renderLeftComponent={(props: NavigationTransitionProps) => {
|
||||
const header = this.props.router.getScreenConfig(props.navigation, 'header') || {};
|
||||
return header.left;
|
||||
}}
|
||||
renderRightComponent={({ scene }) => {
|
||||
const navigation = this._getChildNavigation(scene);
|
||||
const header = this.props.router.getScreenConfig(navigation, 'header');
|
||||
if (header && header.right) {
|
||||
return header.right;
|
||||
}
|
||||
return null;
|
||||
renderRightComponent={(props: NavigationTransitionProps) => {
|
||||
const header = this.props.router.getScreenConfig(props.navigation, 'header') || {};
|
||||
return header.right;
|
||||
}}
|
||||
renderTitleComponent={({ scene }) => {
|
||||
const navigation = this._getChildNavigation(scene);
|
||||
const header = this.props.router.getScreenConfig(navigation, 'header');
|
||||
let title = null;
|
||||
if (header && header.title) {
|
||||
title = header.title;
|
||||
} else {
|
||||
title = this.props.router.getScreenConfig(navigation, 'title');
|
||||
renderTitleComponent={(props: NavigationTransitionProps) => {
|
||||
const header = this.props.router.getScreenConfig(props.navigation, 'header') || {};
|
||||
// When we return 'undefined' from 'renderXComponent', header treats them as not
|
||||
// specified and default 'renderXComponent' functions are used. In case of 'title',
|
||||
// we return 'undefined' in case of 'string' too because the default 'renderTitle'
|
||||
// function in header handles them.
|
||||
if (typeof header.title === 'string') {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof title === 'string') {
|
||||
return <Header.Title>{title}</Header.Title>;
|
||||
}
|
||||
return title;
|
||||
return header.title;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -254,6 +246,7 @@ class CardStack extends React.Component<DefaultProps, Props, void> {
|
||||
scene => this._renderScene({
|
||||
...props,
|
||||
scene,
|
||||
navigation: this._getChildNavigation(scene),
|
||||
})
|
||||
)}
|
||||
</View>
|
||||
@@ -297,8 +290,7 @@ class CardStack extends React.Component<DefaultProps, Props, void> {
|
||||
Component: ReactClass<*>,
|
||||
props: NavigationSceneRendererProps,
|
||||
): React.Element<*> {
|
||||
const navigation = this._getChildNavigation(props.scene);
|
||||
const header = this.props.router.getScreenConfig(navigation, 'header');
|
||||
const header = this.props.router.getScreenConfig(props.navigation, 'header');
|
||||
const headerMode = this._getHeaderMode();
|
||||
if (headerMode === 'screen') {
|
||||
const isHeaderHidden = header && header.visible === false;
|
||||
@@ -309,7 +301,7 @@ class CardStack extends React.Component<DefaultProps, Props, void> {
|
||||
{maybeHeader}
|
||||
<SceneView
|
||||
screenProps={this.props.screenProps}
|
||||
navigation={navigation}
|
||||
navigation={props.navigation}
|
||||
component={Component}
|
||||
/>
|
||||
</View>
|
||||
@@ -318,7 +310,7 @@ class CardStack extends React.Component<DefaultProps, Props, void> {
|
||||
return (
|
||||
<SceneView
|
||||
screenProps={this.props.screenProps}
|
||||
navigation={navigation}
|
||||
navigation={props.navigation}
|
||||
component={Component}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -15,29 +15,27 @@ import HeaderTitle from './HeaderTitle';
|
||||
import HeaderBackButton from './HeaderBackButton';
|
||||
import HeaderStyleInterpolator from './HeaderStyleInterpolator';
|
||||
import NavigationPropTypes from '../PropTypes';
|
||||
import TransitionConfigs from './TransitionConfigs';
|
||||
import addNavigationHelpers from '../addNavigationHelpers';
|
||||
|
||||
import type {
|
||||
NavigationScene,
|
||||
NavigationRouter,
|
||||
NavigationRoute,
|
||||
NavigationAction,
|
||||
NavigationScreenProp,
|
||||
NavigationSceneRendererProps,
|
||||
NavigationStyleInterpolator,
|
||||
} from '../TypeDefinition';
|
||||
|
||||
import type { TransitionConfig } from './TransitionConfigs';
|
||||
|
||||
export type HeaderMode = 'float' | 'screen' | 'none';
|
||||
|
||||
type SubViewProps = NavigationSceneRendererProps & {
|
||||
onNavigateBack: ?() => void,
|
||||
};
|
||||
|
||||
type SubViewRenderer = (subViewProps: SubViewProps) => ?React.Element<*>;
|
||||
type Navigation = NavigationScreenProp<NavigationRoute, NavigationAction>;
|
||||
|
||||
type DefaultProps = {
|
||||
renderLeftComponent: SubViewRenderer,
|
||||
renderRightComponent: SubViewRenderer,
|
||||
renderTitleComponent: SubViewRenderer,
|
||||
};
|
||||
type SubViewRenderer = (subViewProps: SubViewProps) => ?React.Element<*>;
|
||||
|
||||
export type HeaderProps = NavigationSceneRendererProps & {
|
||||
mode: HeaderMode,
|
||||
@@ -45,6 +43,8 @@ export type HeaderProps = NavigationSceneRendererProps & {
|
||||
renderLeftComponent: SubViewRenderer,
|
||||
renderRightComponent: SubViewRenderer,
|
||||
renderTitleComponent: SubViewRenderer,
|
||||
tintColor: ?string,
|
||||
router: NavigationRouter,
|
||||
style?: any,
|
||||
};
|
||||
|
||||
@@ -53,32 +53,12 @@ type SubViewName = 'left' | 'title' | 'right';
|
||||
const APPBAR_HEIGHT = Platform.OS === 'ios' ? 44 : 56;
|
||||
const STATUSBAR_HEIGHT = Platform.OS === 'ios' ? 20 : 0;
|
||||
|
||||
class Header extends React.Component<DefaultProps, HeaderProps, *> {
|
||||
class Header extends React.Component<void, HeaderProps, void> {
|
||||
|
||||
static HEIGHT = APPBAR_HEIGHT + STATUSBAR_HEIGHT;
|
||||
static Title = HeaderTitle;
|
||||
static BackButton = HeaderBackButton;
|
||||
|
||||
static defaultProps = {
|
||||
renderTitleComponent: (props: SubViewProps) => {
|
||||
const title = String(props.scene.route.title || '');
|
||||
return <HeaderTitle>{title}</HeaderTitle>;
|
||||
},
|
||||
|
||||
renderLeftComponent: (props: SubViewProps) => {
|
||||
if (props.scene.index === 0 || !props.onNavigateBack) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<HeaderBackButton
|
||||
onPress={props.onNavigateBack}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
renderRightComponent: () => null,
|
||||
};
|
||||
|
||||
// propTypes for people who don't use Flow
|
||||
static propTypes = {
|
||||
...NavigationPropTypes.SceneRendererProps,
|
||||
@@ -86,6 +66,7 @@ class Header extends React.Component<DefaultProps, HeaderProps, *> {
|
||||
renderLeftComponent: PropTypes.func,
|
||||
renderRightComponent: PropTypes.func,
|
||||
renderTitleComponent: PropTypes.func,
|
||||
router: PropTypes.object,
|
||||
style: PropTypes.any,
|
||||
};
|
||||
|
||||
@@ -99,11 +80,60 @@ class Header extends React.Component<DefaultProps, HeaderProps, *> {
|
||||
);
|
||||
}
|
||||
|
||||
_getHeaderTitle(navigation: Navigation): ?string {
|
||||
const header = this.props.router.getScreenConfig(navigation, 'header');
|
||||
let title;
|
||||
if (header && header.title) {
|
||||
title = header.title;
|
||||
} else {
|
||||
title = this.props.router.getScreenConfig(navigation, 'title');
|
||||
}
|
||||
return typeof title === 'string' ? title : undefined;
|
||||
}
|
||||
|
||||
_getHeaderTintColor(navigation: Navigation): ?string {
|
||||
const header = this.props.router.getScreenConfig(navigation, 'header');
|
||||
if (header && header.tintColor) {
|
||||
return header.tintColor;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
_renderTitleComponent = (props: SubViewProps) => {
|
||||
const color = this._getHeaderTintColor(props.navigation);
|
||||
const title = this._getHeaderTitle(props.navigation);
|
||||
return <HeaderTitle style={color && ({ color })}>{title}</HeaderTitle>;
|
||||
};
|
||||
|
||||
_renderLeftComponent = (props: SubViewProps) => {
|
||||
if (props.scene.index === 0 || !props.onNavigateBack) {
|
||||
return null;
|
||||
}
|
||||
const tintColor = this._getHeaderTintColor(props.navigation);
|
||||
const previousNavigation = addNavigationHelpers({
|
||||
...props.navigation,
|
||||
state: props.scenes[props.scene.index - 1].route,
|
||||
});
|
||||
const backButtonTitle = this._getHeaderTitle(previousNavigation);
|
||||
return (
|
||||
<HeaderBackButton
|
||||
onPress={props.onNavigateBack}
|
||||
tintColor={tintColor}
|
||||
title={backButtonTitle}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
_renderRightComponent = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
_renderLeft(props: NavigationSceneRendererProps): ?React.Element<*> {
|
||||
return this._renderSubView(
|
||||
props,
|
||||
'left',
|
||||
this.props.renderLeftComponent,
|
||||
this._renderLeftComponent,
|
||||
HeaderStyleInterpolator.forLeft,
|
||||
);
|
||||
}
|
||||
@@ -124,6 +154,7 @@ class Header extends React.Component<DefaultProps, HeaderProps, *> {
|
||||
{ ...props, style },
|
||||
'title',
|
||||
this.props.renderTitleComponent,
|
||||
this._renderTitleComponent,
|
||||
HeaderStyleInterpolator.forCenter,
|
||||
);
|
||||
}
|
||||
@@ -133,6 +164,7 @@ class Header extends React.Component<DefaultProps, HeaderProps, *> {
|
||||
props,
|
||||
'right',
|
||||
this.props.renderRightComponent,
|
||||
this._renderRightComponent,
|
||||
HeaderStyleInterpolator.forRight,
|
||||
);
|
||||
}
|
||||
@@ -141,6 +173,7 @@ class Header extends React.Component<DefaultProps, HeaderProps, *> {
|
||||
props: NavigationSceneRendererProps,
|
||||
name: SubViewName,
|
||||
renderer: SubViewRenderer,
|
||||
defaultRenderer: SubViewRenderer,
|
||||
styleInterpolator: NavigationStyleInterpolator,
|
||||
): ?React.Element<*> {
|
||||
const {
|
||||
@@ -165,7 +198,12 @@ class Header extends React.Component<DefaultProps, HeaderProps, *> {
|
||||
...props,
|
||||
onNavigateBack: this.props.onNavigateBack,
|
||||
};
|
||||
const subView = renderer(subViewProps);
|
||||
|
||||
let subView = renderer(subViewProps);
|
||||
if (subView === undefined) {
|
||||
subView = defaultRenderer(subViewProps);
|
||||
}
|
||||
|
||||
if (subView === null) {
|
||||
return null;
|
||||
}
|
||||
@@ -200,6 +238,10 @@ class Header extends React.Component<DefaultProps, HeaderProps, *> {
|
||||
const props = NavigationPropTypes.extractSceneRendererProps(this.props);
|
||||
props.scene = scene;
|
||||
props.index = index;
|
||||
props.navigation = addNavigationHelpers({
|
||||
...this.props.navigation,
|
||||
state: scene.route,
|
||||
});
|
||||
return props;
|
||||
}): Array<NavigationSceneRendererProps>);
|
||||
leftComponents = scenesProps.map(this._renderLeft, this);
|
||||
@@ -227,8 +269,8 @@ class Header extends React.Component<DefaultProps, HeaderProps, *> {
|
||||
return (
|
||||
<Animated.View {...rest} style={[styles.container, style]}>
|
||||
<View style={styles.appBar}>
|
||||
{leftComponents}
|
||||
{titleComponents}
|
||||
{leftComponents}
|
||||
{rightComponents}
|
||||
</View>
|
||||
</Animated.View>
|
||||
|
||||
@@ -4,6 +4,8 @@ import React, { PropTypes } from 'react';
|
||||
import {
|
||||
I18nManager,
|
||||
Image,
|
||||
Text,
|
||||
View,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
@@ -12,21 +14,29 @@ import TouchableItem from './TouchableItem';
|
||||
|
||||
type Props = {
|
||||
onPress: Function,
|
||||
title?: string,
|
||||
tintColor?: string;
|
||||
};
|
||||
|
||||
const HeaderBackButton = ({ onPress, tintColor }: Props) => (
|
||||
const HeaderBackButton = ({ onPress, title, tintColor }: Props) => (
|
||||
<TouchableItem
|
||||
delayPressIn={0}
|
||||
onPress={onPress}
|
||||
style={styles.container}
|
||||
borderless
|
||||
>
|
||||
<Image
|
||||
style={styles.button}
|
||||
source={require('./assets/back-icon.png')}
|
||||
tintColor={tintColor}
|
||||
/>
|
||||
<View style={styles.container}>
|
||||
<Image
|
||||
style={styles.button}
|
||||
source={require('./assets/back-icon.png')}
|
||||
tintColor={tintColor}
|
||||
/>
|
||||
{Platform.OS === 'ios' && title && (
|
||||
<Text style={[styles.title, { color: tintColor }]}>
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</TouchableItem>
|
||||
);
|
||||
|
||||
@@ -35,18 +45,36 @@ HeaderBackButton.propTypes = {
|
||||
tintColor: PropTypes.string,
|
||||
};
|
||||
|
||||
HeaderBackButton.defaultProps = {
|
||||
tintColor: Platform.select({
|
||||
ios: '#037aff',
|
||||
}),
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
button: {
|
||||
height: 24,
|
||||
width: 24,
|
||||
margin: Platform.OS === 'ios' ? 10 : 16,
|
||||
resizeMode: 'contain',
|
||||
transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }],
|
||||
title: {
|
||||
fontSize: 17,
|
||||
},
|
||||
button: Platform.OS === 'ios'
|
||||
? {
|
||||
height: 21,
|
||||
width: 13,
|
||||
margin: 10,
|
||||
marginRight: 5,
|
||||
transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }],
|
||||
}
|
||||
: {
|
||||
height: 24,
|
||||
width: 24,
|
||||
margin: 16,
|
||||
resizeMode: 'contain',
|
||||
transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }],
|
||||
},
|
||||
});
|
||||
|
||||
export default HeaderBackButton;
|
||||
|
||||
@@ -4,7 +4,6 @@ import React from 'react';
|
||||
|
||||
import {
|
||||
Animated,
|
||||
Easing,
|
||||
StyleSheet,
|
||||
View,
|
||||
} from 'react-native';
|
||||
@@ -12,14 +11,17 @@ import {
|
||||
import invariant from 'fbjs/lib/invariant';
|
||||
|
||||
import NavigationScenesReducer from './ScenesReducer';
|
||||
import NavigationPropTypes from '../PropTypes';
|
||||
import TransitionConfigs from './TransitionConfigs';
|
||||
import addNavigationHelpers from '../addNavigationHelpers';
|
||||
import NavigationPropTypes from '../PropTypes';
|
||||
|
||||
import type {
|
||||
NavigationAnimatedValue,
|
||||
NavigationLayout,
|
||||
NavigationScene,
|
||||
NavigationState,
|
||||
NavigationRoute,
|
||||
NavigationAction,
|
||||
NavigationScreenProp,
|
||||
NavigationTransitionProps,
|
||||
NavigationTransitionSpec,
|
||||
} from '../TypeDefinition';
|
||||
@@ -29,7 +31,7 @@ type Props = {
|
||||
transitionProps: NavigationTransitionProps,
|
||||
prevTransitionProps: ?NavigationTransitionProps,
|
||||
) => NavigationTransitionSpec,
|
||||
navigationState: NavigationState,
|
||||
navigation: NavigationScreenProp<NavigationRoute, NavigationAction>,
|
||||
onTransitionEnd: () => void,
|
||||
onTransitionStart: () => void,
|
||||
render: (
|
||||
@@ -62,7 +64,9 @@ class Transitioner extends React.Component<*, Props, State> {
|
||||
|
||||
static propTypes = {
|
||||
configureTransition: PropTypes.func,
|
||||
navigationState: NavigationPropTypes.navigationState.isRequired,
|
||||
navigation: PropTypes.shape({
|
||||
state: NavigationPropTypes.navigationState.isRequired,
|
||||
}).isRequired,
|
||||
onTransitionEnd: PropTypes.func,
|
||||
onTransitionStart: PropTypes.func,
|
||||
render: PropTypes.func.isRequired,
|
||||
@@ -83,9 +87,9 @@ class Transitioner extends React.Component<*, Props, State> {
|
||||
|
||||
this.state = {
|
||||
layout,
|
||||
position: new Animated.Value(this.props.navigationState.index),
|
||||
position: new Animated.Value(this.props.navigation.state.index),
|
||||
progress: new Animated.Value(1),
|
||||
scenes: NavigationScenesReducer([], this.props.navigationState),
|
||||
scenes: NavigationScenesReducer([], this.props.navigation.state),
|
||||
};
|
||||
|
||||
this._prevTransitionProps = null;
|
||||
@@ -109,8 +113,8 @@ class Transitioner extends React.Component<*, Props, State> {
|
||||
componentWillReceiveProps(nextProps: Props): void {
|
||||
const nextScenes = NavigationScenesReducer(
|
||||
this.state.scenes,
|
||||
nextProps.navigationState,
|
||||
this.props.navigationState
|
||||
nextProps.navigation.state,
|
||||
this.props.navigation.state
|
||||
);
|
||||
|
||||
if (nextScenes === this.state.scenes) {
|
||||
@@ -158,13 +162,13 @@ class Transitioner extends React.Component<*, Props, State> {
|
||||
),
|
||||
];
|
||||
|
||||
if (nextProps.navigationState.index !== this.props.navigationState.index) {
|
||||
if (nextProps.navigation.state.index !== this.props.navigation.state.index) {
|
||||
animations.push(
|
||||
timing(
|
||||
position,
|
||||
{
|
||||
...transitionSpec,
|
||||
toValue: nextProps.navigationState.index,
|
||||
toValue: nextProps.navigation.state.index,
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -244,7 +248,7 @@ function buildTransitionProps(
|
||||
state: State,
|
||||
): NavigationTransitionProps {
|
||||
const {
|
||||
navigationState,
|
||||
navigation,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
@@ -260,7 +264,11 @@ function buildTransitionProps(
|
||||
|
||||
return {
|
||||
layout,
|
||||
navigationState,
|
||||
navigationState: navigation.state,
|
||||
navigation: addNavigationHelpers({
|
||||
...navigation,
|
||||
state: scene.route,
|
||||
}),
|
||||
position,
|
||||
progress,
|
||||
scenes,
|
||||
|
||||
|
Before Width: | Height: | Size: 177 B After Width: | Height: | Size: 518 B |
|
Before Width: | Height: | Size: 273 B After Width: | Height: | Size: 792 B |
|
Before Width: | Height: | Size: 303 B After Width: | Height: | Size: 950 B |
|
Before Width: | Height: | Size: 439 B After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 1.8 KiB |