Refactor header, tintColor support, redesign iOS back button (#109)

This commit is contained in:
Mike Grabowski
2017-02-01 22:20:12 +01:00
committed by Satyajit Sahoo
parent 736d0db732
commit 7a20389e04
13 changed files with 172 additions and 97 deletions

View File

@@ -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,
};
}

View File

@@ -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,

View File

@@ -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}
/>
);

View File

@@ -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>

View File

@@ -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;

View File

@@ -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,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 B

After

Width:  |  Height:  |  Size: 518 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 273 B

After

Width:  |  Height:  |  Size: 792 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 303 B

After

Width:  |  Height:  |  Size: 950 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 439 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 1.8 KiB