mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-02-11 22:33:32 +08:00
261 lines
6.3 KiB
TypeScript
261 lines
6.3 KiB
TypeScript
import * as React from 'react';
|
|
import {
|
|
I18nManager,
|
|
Image,
|
|
View,
|
|
Platform,
|
|
StyleSheet,
|
|
LayoutChangeEvent,
|
|
MaskedViewIOS,
|
|
} from 'react-native';
|
|
import Animated from 'react-native-reanimated';
|
|
import TouchableItem from '../TouchableItem';
|
|
import { HeaderBackButtonProps } from '../../types';
|
|
|
|
type Props = HeaderBackButtonProps & {
|
|
tintColor: string;
|
|
};
|
|
|
|
type State = {
|
|
initialLabelWidth?: number;
|
|
};
|
|
|
|
class HeaderBackButton extends React.Component<Props, State> {
|
|
static defaultProps = {
|
|
pressColorAndroid: 'rgba(0, 0, 0, .32)',
|
|
tintColor: Platform.select({
|
|
ios: '#037aff',
|
|
web: '#5f6368',
|
|
}),
|
|
labelVisible: Platform.OS === 'ios',
|
|
truncatedLabel: 'Back',
|
|
};
|
|
|
|
state: State = {};
|
|
|
|
private handleLabelLayout = (e: LayoutChangeEvent) => {
|
|
const { onLabelLayout } = this.props;
|
|
|
|
onLabelLayout && onLabelLayout(e);
|
|
|
|
if (this.state.initialLabelWidth) {
|
|
return;
|
|
}
|
|
|
|
// This measurement is used to determine if we should truncate the label when it doesn't fit
|
|
// Only measure it once because `onLayout` will fire again when we render truncated label
|
|
// and we want the measurement of not-truncated label
|
|
this.setState({
|
|
initialLabelWidth: e.nativeEvent.layout.x + e.nativeEvent.layout.width,
|
|
});
|
|
};
|
|
|
|
private shouldTruncateLabel = () => {
|
|
const { titleLayout, screenLayout, label } = this.props;
|
|
const { initialLabelWidth } = this.state;
|
|
|
|
return (
|
|
!label ||
|
|
(initialLabelWidth &&
|
|
titleLayout &&
|
|
screenLayout &&
|
|
(screenLayout.width - titleLayout.width) / 2 < initialLabelWidth + 26)
|
|
);
|
|
};
|
|
|
|
private renderBackImage = () => {
|
|
const { backImage, labelVisible, tintColor } = this.props;
|
|
|
|
if (backImage) {
|
|
return backImage({ tintColor });
|
|
} else {
|
|
return (
|
|
<Image
|
|
style={[
|
|
styles.icon,
|
|
Boolean(labelVisible) && styles.iconWithLabel,
|
|
Boolean(tintColor) && { tintColor },
|
|
]}
|
|
source={require('../assets/back-icon.png')}
|
|
fadeDuration={0}
|
|
/>
|
|
);
|
|
}
|
|
};
|
|
|
|
private renderLabel() {
|
|
const {
|
|
label,
|
|
truncatedLabel,
|
|
allowFontScaling,
|
|
labelVisible,
|
|
backImage,
|
|
labelStyle,
|
|
tintColor,
|
|
screenLayout,
|
|
} = this.props;
|
|
|
|
const leftLabelText = this.shouldTruncateLabel() ? truncatedLabel : label;
|
|
|
|
if (!labelVisible || leftLabelText === undefined) {
|
|
return null;
|
|
}
|
|
|
|
const labelElement = (
|
|
<Animated.Text
|
|
accessible={false}
|
|
onLayout={this.handleLabelLayout}
|
|
style={[
|
|
styles.label,
|
|
tintColor ? { color: tintColor } : null,
|
|
labelStyle,
|
|
]}
|
|
numberOfLines={1}
|
|
allowFontScaling={!!allowFontScaling}
|
|
>
|
|
{leftLabelText}
|
|
</Animated.Text>
|
|
);
|
|
|
|
if (backImage || Platform.OS !== 'ios') {
|
|
// When a custom backimage is specified, we can't mask the label
|
|
// Otherwise there might be weird effect due to our mask not being the same as the image
|
|
return labelElement;
|
|
}
|
|
|
|
return (
|
|
<MaskedViewIOS
|
|
maskElement={
|
|
<View style={styles.iconMaskContainer}>
|
|
<Image
|
|
source={require('../assets/back-icon-mask.png')}
|
|
style={styles.iconMask}
|
|
/>
|
|
<View style={styles.iconMaskFillerRect} />
|
|
</View>
|
|
}
|
|
>
|
|
<View
|
|
style={
|
|
screenLayout
|
|
? // We make the button extend till the middle of the screen
|
|
// Otherwise it appears to cut off when translating
|
|
[styles.labelWrapper, { minWidth: screenLayout.width / 2 - 27 }]
|
|
: null
|
|
}
|
|
>
|
|
{labelElement}
|
|
</View>
|
|
</MaskedViewIOS>
|
|
);
|
|
}
|
|
|
|
private handlePress = () =>
|
|
this.props.onPress && requestAnimationFrame(this.props.onPress);
|
|
|
|
render() {
|
|
const { pressColorAndroid, label, disabled } = this.props;
|
|
|
|
return (
|
|
<TouchableItem
|
|
disabled={disabled}
|
|
accessible
|
|
accessibilityRole="button"
|
|
accessibilityComponentType="button"
|
|
accessibilityLabel={
|
|
label && label !== 'Back' ? `${label}, back` : 'Go back'
|
|
}
|
|
accessibilityTraits="button"
|
|
testID="header-back"
|
|
delayPressIn={0}
|
|
onPress={disabled ? undefined : this.handlePress}
|
|
pressColor={pressColorAndroid}
|
|
style={[styles.container, disabled && styles.disabled]}
|
|
hitSlop={Platform.select({
|
|
ios: undefined,
|
|
default: { top: 8, right: 8, bottom: 8, left: 8 },
|
|
})}
|
|
borderless
|
|
>
|
|
<React.Fragment>
|
|
{this.renderBackImage()}
|
|
{this.renderLabel()}
|
|
</React.Fragment>
|
|
</TouchableItem>
|
|
);
|
|
}
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
alignItems: 'center',
|
|
flexDirection: 'row',
|
|
...Platform.select({
|
|
ios: null,
|
|
default: {
|
|
marginVertical: 3,
|
|
marginHorizontal: 11,
|
|
},
|
|
}),
|
|
},
|
|
disabled: {
|
|
opacity: 0.5,
|
|
},
|
|
label: {
|
|
fontSize: 17,
|
|
// Title and back label are a bit different width due to title being bold
|
|
// Adjusting the letterSpacing makes them coincide better
|
|
letterSpacing: 0.35,
|
|
},
|
|
labelWrapper: {
|
|
// These styles will make sure that the label doesn't fill the available space
|
|
// Otherwise it messes with the measurement of the label
|
|
flexDirection: 'row',
|
|
alignItems: 'flex-start',
|
|
},
|
|
icon: Platform.select({
|
|
ios: {
|
|
height: 21,
|
|
width: 13,
|
|
marginLeft: 8,
|
|
marginRight: 22,
|
|
marginVertical: 12,
|
|
resizeMode: 'contain',
|
|
transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }],
|
|
},
|
|
default: {
|
|
height: 24,
|
|
width: 24,
|
|
margin: 3,
|
|
resizeMode: 'contain',
|
|
transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }],
|
|
},
|
|
}),
|
|
iconWithLabel:
|
|
Platform.OS === 'ios'
|
|
? {
|
|
marginRight: 6,
|
|
}
|
|
: {},
|
|
iconMaskContainer: {
|
|
flex: 1,
|
|
flexDirection: 'row',
|
|
justifyContent: 'center',
|
|
},
|
|
iconMaskFillerRect: {
|
|
flex: 1,
|
|
backgroundColor: '#000',
|
|
},
|
|
iconMask: {
|
|
height: 21,
|
|
width: 13,
|
|
marginLeft: -14.5,
|
|
marginVertical: 12,
|
|
alignSelf: 'center',
|
|
resizeMode: 'contain',
|
|
transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }],
|
|
},
|
|
});
|
|
|
|
export default HeaderBackButton;
|