mirror of
https://github.com/status-im/react-navigation.git
synced 2025-02-24 09:08:15 +00:00
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:
parent
3c36db455f
commit
cd3707d64b
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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 }],
|
||||
},
|
||||
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user