Add headerLayoutPreset, add config for back button title visibility and make it have reasonable defaults, better back button ripple on Android (#4588)

This commit is contained in:
Brent Vatne 2018-07-20 14:12:39 -07:00 committed by GitHub
parent 3c36db455f
commit cd3707d64b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 217 additions and 89 deletions

View File

@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- `headerLayoutPreset: 'center' | 'left'` to provide an easy solution for [questions like this](https://github.com/react-navigation/react-navigation/issues/4615).
- `headerBackTitleEnabled` - this configuration option for stack navigator allows you to force back button titles to either be rendered or not (if you disagree with defaults for your platform and layout preset).
### Fixed
- Android back button ripple is now appropriately sized (fixes [#3955](https://github.com/react-navigation/react-navigation/issues/3955)).
## [2.8.0] - [2018-07-19](https://github.com/react-navigation/react-navigation/releases/tag/2.8.0)
### Added

View File

@ -10,7 +10,7 @@ import type {
} from 'react-navigation';
import * as React from 'react';
import { ScrollView, StatusBar } from 'react-native';
import { Platform, ScrollView, StatusBar } from 'react-native';
import {
createStackNavigator,
SafeAreaView,
@ -24,6 +24,8 @@ import SampleText from './SampleText';
import { Button } from './commonComponents/ButtonWithMargin';
import { HeaderButtons } from './commonComponents/HeaderButtons';
const DEBUG = false;
type MyNavScreenProps = {
navigation: NavigationScreenProp<NavigationState>,
banner: React.Node,
@ -133,16 +135,16 @@ class MyHomeScreen extends React.Component<MyHomeScreenProps> {
this._s3.remove();
}
_onWF = a => {
console.log('_willFocus HomeScreen', a);
DEBUG && console.log('_willFocus HomeScreen', a);
};
_onDF = a => {
console.log('_didFocus HomeScreen', a);
DEBUG && console.log('_didFocus HomeScreen', a);
};
_onWB = a => {
console.log('_willBlur HomeScreen', a);
DEBUG && console.log('_willBlur HomeScreen', a);
};
_onDB = a => {
console.log('_didBlur HomeScreen', a);
DEBUG && console.log('_didBlur HomeScreen', a);
};
render() {
@ -231,18 +233,23 @@ MyProfileScreen.navigationOptions = props => {
};
};
const SimpleStack = createStackNavigator({
Home: {
screen: MyHomeScreen,
const SimpleStack = createStackNavigator(
{
Home: {
screen: MyHomeScreen,
},
Profile: {
path: 'people/:name',
screen: MyProfileScreen,
},
Photos: {
path: 'photos/:name',
screen: MyPhotosScreen,
},
},
Profile: {
path: 'people/:name',
screen: MyProfileScreen,
},
Photos: {
path: 'photos/:name',
screen: MyPhotosScreen,
},
});
{
// headerLayoutPreset: 'center',
}
);
export default SimpleStack;

View File

@ -21,7 +21,47 @@ import withOrientation from '../withOrientation';
const APPBAR_HEIGHT = Platform.OS === 'ios' ? 44 : 56;
const STATUSBAR_HEIGHT = Platform.OS === 'ios' ? 20 : 0;
const TITLE_OFFSET = Platform.OS === 'ios' ? 70 : 56;
// These can be adjusted by using headerTitleContainerStyle on navigationOptions
const TITLE_OFFSET_CENTER_ALIGN = Platform.OS === 'ios' ? 70 : 56;
const TITLE_OFFSET_LEFT_ALIGN = Platform.OS === 'ios' ? 20 : 56;
const getTitleOffsets = (
layoutPreset,
forceBackTitle,
hasLeftComponent,
hasRightComponent
) => {
if (layoutPreset === 'left') {
// Maybe at some point we should do something different if the back title is
// explicitly enabled, for now people can control it manually
let style = {
left: TITLE_OFFSET_LEFT_ALIGN,
right: TITLE_OFFSET_LEFT_ALIGN,
};
if (!hasLeftComponent) {
style.left = 0;
}
if (!hasRightComponent) {
style.right = 0;
}
return style;
} else if (layoutPreset === 'center') {
let style = {
left: TITLE_OFFSET_CENTER_ALIGN,
right: TITLE_OFFSET_CENTER_ALIGN,
};
if (!hasLeftComponent && !hasRightComponent) {
style.left = 0;
style.right = 0;
}
return style;
}
};
const getAppBarHeight = isLandscape => {
return Platform.OS === 'ios'
@ -92,6 +132,7 @@ class Header extends React.PureComponent {
}
_renderTitleComponent = props => {
const { layoutPreset } = this.props;
const { options } = props.scene.descriptor;
const headerTitle = options.headerTitle;
if (React.isValidElement(headerTitle)) {
@ -103,10 +144,10 @@ class Header extends React.PureComponent {
const color = options.headerTintColor;
const allowFontScaling = options.headerTitleAllowFontScaling;
// On iOS, width of left/right components depends on the calculated
// size of the title.
const onLayoutIOS =
Platform.OS === 'ios'
// When title is centered, the width of left/right components depends on the
// calculated size of the title.
const onLayout =
layoutPreset === 'center'
? e => {
this.setState({
widths: {
@ -117,18 +158,24 @@ class Header extends React.PureComponent {
}
: undefined;
const RenderedHeaderTitle =
const HeaderTitleComponent =
headerTitle && typeof headerTitle !== 'string'
? headerTitle
: HeaderTitle;
return (
<RenderedHeaderTitle
onLayout={onLayoutIOS}
<HeaderTitleComponent
onLayout={onLayout}
allowFontScaling={allowFontScaling == null ? true : allowFontScaling}
style={[color ? { color } : null, titleStyle]}
style={[
color ? { color } : null,
layoutPreset === 'center'
? { textAlign: 'center' }
: { textAlign: 'left' },
titleStyle,
]}
>
{titleString}
</RenderedHeaderTitle>
</HeaderTitleComponent>
);
};
@ -167,7 +214,9 @@ class Header extends React.PureComponent {
backImage={options.headerBackImage}
title={backButtonTitle}
truncatedTitle={truncatedBackButtonTitle}
backTitleVisible={this.props.backTitleVisible}
titleStyle={options.headerBackTitleStyle}
layoutPreset={this.props.layoutPreset}
width={width}
/>
);
@ -251,27 +300,16 @@ class Header extends React.PureComponent {
}
_renderTitle(props, options) {
let style = {};
const { transitionPreset } = this.props;
if (Platform.OS === 'android') {
if (!options.hasLeftComponent) {
style.left = 0;
}
if (!options.hasRightComponent) {
style.right = 0;
}
} else if (
Platform.OS === 'ios' &&
!options.hasLeftComponent &&
!options.hasRightComponent
) {
style.left = 0;
style.right = 0;
}
if (options.headerTitleContainerStyle) {
style = [style, options.headerTitleContainerStyle];
}
const { layoutPreset, transitionPreset } = this.props;
let style = [
{ justifyContent: layoutPreset === 'center' ? 'center' : 'flex-start' },
getTitleOffsets(
layoutPreset,
options.hasLeftComponent,
options.hasRightComponent
),
options.headerTitleContainerStyle,
];
return this._renderSubView(
{ ...props, style },
@ -386,7 +424,6 @@ class Header extends React.PureComponent {
styles[name],
props.style,
styleInterpolator({
// todo: determine if we really need to splat all this.props
...this.props,
...props,
}),
@ -636,12 +673,9 @@ const styles = StyleSheet.create({
title: {
bottom: 0,
top: 0,
left: TITLE_OFFSET,
right: TITLE_OFFSET,
position: 'absolute',
alignItems: 'center',
flexDirection: 'row',
justifyContent: Platform.OS === 'ios' ? 'center' : 'flex-start',
},
left: {
left: 0,

View File

@ -62,9 +62,38 @@ class HeaderBackButton extends React.PureComponent {
}
render() {
const { onPress, pressColorAndroid, layoutPreset, title } = this.props;
let button = (
<TouchableItem
accessibilityComponentType="button"
accessibilityLabel={title}
accessibilityTraits="button"
testID="header-back"
delayPressIn={0}
onPress={onPress}
pressColor={pressColorAndroid}
style={styles.container}
borderless
>
<View style={styles.container}>
{this._renderBackImage()}
{this._maybeRenderTitle()}
</View>
</TouchableItem>
);
if (Platform.OS === 'android') {
return <View style={styles.androidButtonWrapper}>{button}</View>;
} else {
return button;
}
}
_maybeRenderTitle() {
const {
onPress,
pressColorAndroid,
layoutPreset,
backTitleVisible,
width,
title,
titleStyle,
@ -79,41 +108,35 @@ class HeaderBackButton extends React.PureComponent {
const backButtonTitle = renderTruncated ? truncatedTitle : title;
// If the left preset is used and we aren't on Android, then we
// default to disabling the label
const titleDefaultsToDisabled =
layoutPreset === 'left' ||
Platform.OS === 'android' ||
typeof backButtonTitle !== 'string';
// If the title is explicitly enabled then we respect that
if (titleDefaultsToDisabled && !backTitleVisible) {
return null;
}
return (
<TouchableItem
accessibilityComponentType="button"
accessibilityLabel={backButtonTitle}
accessibilityTraits="button"
testID="header-back"
delayPressIn={0}
onPress={onPress}
pressColor={pressColorAndroid}
style={styles.container}
borderless
<Text
onLayout={this._onTextLayout}
style={[styles.title, !!tintColor && { color: tintColor }, titleStyle]}
numberOfLines={1}
>
<View style={styles.container}>
{this._renderBackImage()}
{Platform.OS === 'ios' &&
typeof backButtonTitle === 'string' && (
<Text
onLayout={this._onTextLayout}
style={[
styles.title,
!!tintColor && { color: tintColor },
titleStyle,
]}
numberOfLines={1}
>
{backButtonTitle}
</Text>
)}
</View>
</TouchableItem>
{backButtonTitle}
</Text>
);
}
}
const styles = StyleSheet.create({
androidButtonWrapper: {
margin: 13,
backgroundColor: 'transparent',
},
container: {
alignItems: 'center',
flexDirection: 'row',
@ -137,7 +160,7 @@ const styles = StyleSheet.create({
: {
height: 24,
width: 24,
margin: 16,
margin: 3,
resizeMode: 'contain',
transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }],
},

View File

@ -17,7 +17,6 @@ const styles = StyleSheet.create({
fontSize: Platform.OS === 'ios' ? 17 : 20,
fontWeight: Platform.OS === 'ios' ? '700' : '500',
color: 'rgba(0, 0, 0, .9)',
textAlign: Platform.OS === 'ios' ? 'center' : 'left',
marginHorizontal: 16,
},
});

View File

@ -34,6 +34,12 @@ const IS_IPHONE_X =
const EaseInOut = Easing.inOut(Easing.ease);
/**
* Enumerate possible values for validation
*/
const HEADER_LAYOUT_PRESET_VALUES = ['center', 'left'];
const HEADER_TRANSITION_PRESET_VALUES = ['uikit', 'fade-in-place'];
/**
* The max duration of the card animation in milliseconds after released gesture.
* The actual duration should be always less then that because the rest distance
@ -159,6 +165,8 @@ class StackViewLayout extends React.Component {
scene,
mode: headerMode,
transitionPreset: this._getHeaderTransitionPreset(),
layoutPreset: this._getHeaderLayoutPreset(),
backTitleVisible: this._getheaderBackTitleVisible(),
leftInterpolator: headerLeftInterpolator,
titleInterpolator: headerTitleInterpolator,
rightInterpolator: headerRightInterpolator,
@ -477,6 +485,40 @@ class StackViewLayout extends React.Component {
return 'float';
}
_getHeaderLayoutPreset() {
const { headerLayoutPreset } = this.props;
if (headerLayoutPreset) {
if (__DEV__) {
if (
this._getHeaderTransitionPreset() === 'uitkit' &&
headerLayoutPreset === 'left' &&
Platform.OS === 'ios'
) {
console.warn(
`headerTransitionPreset with the value 'ui-kit' is incompatible with headerLayoutPreset 'left'`
);
}
}
if (HEADER_LAYOUT_PRESET_VALUES.includes(headerLayoutPreset)) {
return headerLayoutPreset;
}
if (__DEV__) {
console.error(
`Invalid configuration applied for headerLayoutPreset - expected one of ${HEADER_LAYOUT_PRESET_VALUES.join(
', '
)} but received ${JSON.stringify(headerLayoutPreset)}`
);
}
}
if (Platform.OS === 'android') {
return 'left';
} else {
return 'center';
}
}
_getHeaderTransitionPreset() {
// On Android or with header mode screen, we always just use in-place,
// we ignore the option entirely (at least until we have other presets)
@ -484,12 +526,28 @@ class StackViewLayout extends React.Component {
return 'fade-in-place';
}
// TODO: validations: 'fade-in-place' or 'uikit' are valid
if (this.props.headerTransitionPreset) {
return this.props.headerTransitionPreset;
} else {
return 'fade-in-place';
const { headerTransitionPreset } = this.props;
if (headerTransitionPreset) {
if (HEADER_TRANSITION_PRESET_VALUES.includes(headerTransitionPreset)) {
return headerTransitionPreset;
}
if (__DEV__) {
console.error(
`Invalid configuration applied for headerTransitionPreset - expected one of ${HEADER_TRANSITION_PRESET_VALUES.join(
', '
)} but received ${JSON.stringify(headerTransitionPreset)}`
);
}
}
return 'fade-in-place';
}
_getheaderBackTitleVisible() {
const { headerBackTitleVisible } = this.props;
return headerBackTitleVisible;
}
_renderInnerScene(scene) {