diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fcb8a5..b96d40a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/examples/NavigationPlayground/js/SimpleStack.js b/examples/NavigationPlayground/js/SimpleStack.js index 27155ed..196e6ed 100644 --- a/examples/NavigationPlayground/js/SimpleStack.js +++ b/examples/NavigationPlayground/js/SimpleStack.js @@ -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, banner: React.Node, @@ -133,16 +135,16 @@ class MyHomeScreen extends React.Component { 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; diff --git a/src/views/Header/Header.js b/src/views/Header/Header.js index 03ab564..13343bb 100644 --- a/src/views/Header/Header.js +++ b/src/views/Header/Header.js @@ -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 ( - {titleString} - + ); }; @@ -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, diff --git a/src/views/Header/HeaderBackButton.js b/src/views/Header/HeaderBackButton.js index 70c67bf..3313271 100644 --- a/src/views/Header/HeaderBackButton.js +++ b/src/views/Header/HeaderBackButton.js @@ -62,9 +62,38 @@ class HeaderBackButton extends React.PureComponent { } render() { + const { onPress, pressColorAndroid, layoutPreset, title } = this.props; + + let button = ( + + + {this._renderBackImage()} + {this._maybeRenderTitle()} + + + ); + + if (Platform.OS === 'android') { + return {button}; + } 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 ( - - - {this._renderBackImage()} - {Platform.OS === 'ios' && - typeof backButtonTitle === 'string' && ( - - {backButtonTitle} - - )} - - + {backButtonTitle} + ); } } 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 }], }, diff --git a/src/views/Header/HeaderTitle.js b/src/views/Header/HeaderTitle.js index c68d4bd..722842c 100644 --- a/src/views/Header/HeaderTitle.js +++ b/src/views/Header/HeaderTitle.js @@ -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, }, }); diff --git a/src/views/StackView/StackViewLayout.js b/src/views/StackView/StackViewLayout.js index 455a3ca..99fb31f 100644 --- a/src/views/StackView/StackViewLayout.js +++ b/src/views/StackView/StackViewLayout.js @@ -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) {