React Navigation源代码阅读 :views/Header/Header.js

import React from 'react';

import {Animated, Image, Platform, StyleSheet, View,} from 'react-native';
import {MaskedViewIOS} from '../../PlatformHelpers';
import SafeAreaView from 'react-native-safe-area-view';

import HeaderTitle from './HeaderTitle';
import HeaderBackButton from './HeaderBackButton';
import ModularHeaderBackButton from './ModularHeaderBackButton';
import HeaderStyleInterpolator from './HeaderStyleInterpolator';
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;

const getAppBarHeight = isLandscape => {
    return Platform.OS === 'ios'
        ? isLandscape && !Platform.isPad ? 32 : 44
        : 56;
};

/**
 * react-navigation 缺省头部 Header 组件定义,
 * 1. 开发人员可以自定义一个Header组件并通过参数 header 指定给屏幕场景组件,这种情况下,缺省头部 Header 组件不会被使用。
 * 2. 即使开发人员使用缺省的 Header 组件,也可以做不同程度的自定义,比如自定义 headerLeft, headerTitle,headerRight 组件,
 *  或者只是自定义一些字体大小粗细等等
 *
 */
class Header extends React.PureComponent {
    static defaultProps = {
        leftInterpolator: HeaderStyleInterpolator.forLeft,
        leftButtonInterpolator: HeaderStyleInterpolator.forLeftButton,
        leftLabelInterpolator: HeaderStyleInterpolator.forLeftLabel,
        titleFromLeftInterpolator: HeaderStyleInterpolator.forCenterFromLeft,
        titleInterpolator: HeaderStyleInterpolator.forCenter,
        rightInterpolator: HeaderStyleInterpolator.forRight,
    };

    static get HEIGHT() {
        return APPBAR_HEIGHT + STATUSBAR_HEIGHT;
    }

    state = {
        widths: {},
    };

    /**
     * 获取头部标题文本字符串
     * @param scene
     * @return {*}
     * @private
     */
    _getHeaderTitleString(scene) {
        const sceneOptions = this.props.getScreenDetails(scene).options;
        if (typeof sceneOptions.headerTitle === 'string') {
            return sceneOptions.headerTitle;
        }
        return sceneOptions.title;
    }

    /**
     * 获取 scene 上一个屏幕/路由,也就是路由栈中位于屏幕 scene 之下的那个屏幕/路由
     * @param scene
     * @return {*}
     * @private
     */
    _getLastScene(scene) {
        return this.props.scenes.find(s => s.index === scene.index - 1);
    }

    /**
     * 获取返回按钮文本字符串:
     * 1. 如果路由栈中 scene 下面没有其他路由,则返回 null
     * 2. 如果路由栈中 scene 下面的那个路由的参数指定了 headerBackTitle, 返回这个参数的值
     * 3. 否则获取路由栈中 scene 下面那个路由的标题字符串
     * @param scene
     * @return {*}
     * @private
     */
    _getBackButtonTitleString(scene) {
        const lastScene = this._getLastScene(scene);
        if (!lastScene) {
            return null;
        }
        const {headerBackTitle} = this.props.getScreenDetails(lastScene).options;
        if (headerBackTitle || headerBackTitle === null) {
            return headerBackTitle;
        }
        return this._getHeaderTitleString(lastScene);
    }

    /**
     * 获取截断后的返回按钮文本字符串:
     * 1. 如果路由栈中 scene 下面没有其他路由,则返回 null
     * 2. 如果路由栈中 scene 下面的那个路由的参数指定了 headerTruncatedBackTitle, 返回这个参数的值
     * @param scene
     * @return {*}
     * @private
     */
    _getTruncatedBackButtonTitle(scene) {
        const lastScene = this._getLastScene(scene);
        if (!lastScene) {
            return null;
        }
        return this.props.getScreenDetails(lastScene).options
            .headerTruncatedBackTitle;
    }

    /**
     * 返回处理逻辑
     * @private
     */
    _navigateBack = () => {
        requestAnimationFrame(() => {
            this.props.navigation.goBack(this.props.scene.route.key);
        });
    };

    _renderTitleComponent = props => {
        const details = this.props.getScreenDetails(props.scene);
        const headerTitle = details.options.headerTitle;
        if (React.isValidElement(headerTitle)) {
            // 如果外部指定了 headerTitle,使用指定的 headerTitle
            return headerTitle;
        }

        // 获取 title 字符串
        const titleString = this._getHeaderTitleString(props.scene);

        const titleStyle = details.options.headerTitleStyle;
        const color = details.options.headerTintColor;
        const allowFontScaling = details.options.headerTitleAllowFontScaling;

        // On iOS, width of left/right components depends on the calculated
        // size of the title.
        const onLayoutIOS =
            Platform.OS === 'ios'
                ? e => {
                    this.setState({
                        widths: {
                            ...this.state.widths,
                            [props.scene.key]: e.nativeEvent.layout.width,
                        },
                    });
                }
                : undefined;

        const RenderedHeaderTitle =
            headerTitle && typeof headerTitle !== 'string'
                ? headerTitle
                : HeaderTitle;
        return (
            <RenderedHeaderTitle
                onLayout={onLayoutIOS}
                allowFontScaling={allowFontScaling == null ? true : allowFontScaling}
                style={[color ? {color} : null, titleStyle]}
            >
                {titleString}
            </RenderedHeaderTitle>
        );
    };

    _renderLeftComponent = props => {
        const {options} = this.props.getScreenDetails(props.scene);

        // 如果参数提供了有效的 headerLeft,或者 参数中的 headerLeft 为 null (表示要隐藏 headerLeft),
        // 使用参数指定的 headerLeft
        if (
            React.isValidElement(options.headerLeft) ||
            options.headerLeft === null
        ) {
            return options.headerLeft;
        }

        if (props.scene.index === 0) {
            return;
        }

        // 下面是缺省的 header left 的实现
        const backButtonTitle = this._getBackButtonTitleString(props.scene);
        const truncatedBackButtonTitle = this._getTruncatedBackButtonTitle(
            props.scene
        );
        const width = this.state.widths[props.scene.key]
            ? (this.props.layout.initWidth - this.state.widths[props.scene.key]) / 2
            : undefined;
        const RenderedLeftComponent = options.headerLeft || HeaderBackButton;
        return (
            <RenderedLeftComponent
                onPress={this._navigateBack}
                pressColorAndroid={options.headerPressColorAndroid}
                tintColor={options.headerTintColor}
                buttonImage={options.headerBackImage}
                title={backButtonTitle}
                truncatedTitle={truncatedBackButtonTitle}
                titleStyle={options.headerBackTitleStyle}
                width={width}
            />
        );
    };

    _renderModularLeftComponent = (
        props,
        ButtonContainerComponent,
        LabelContainerComponent
    ) => {
        const {options} = this.props.getScreenDetails(props.scene);
        const backButtonTitle = this._getBackButtonTitleString(props.scene);
        const truncatedBackButtonTitle = this._getTruncatedBackButtonTitle(
            props.scene
        );
        const width = this.state.widths[props.scene.key]
            ? (this.props.layout.initWidth - this.state.widths[props.scene.key]) / 2
            : undefined;

        // 下面是缺省的 header left 的实现
        return (
            <ModularHeaderBackButton
                onPress={this._navigateBack}
                ButtonContainerComponent={ButtonContainerComponent}
                LabelContainerComponent={LabelContainerComponent}
                pressColorAndroid={options.headerPressColorAndroid}
                tintColor={options.headerTintColor}
                buttonImage={options.headerBackImage}
                title={backButtonTitle}
                truncatedTitle={truncatedBackButtonTitle}
                titleStyle={options.headerBackTitleStyle}
                width={width}
            />
        );
    };

    _renderRightComponent = props => {
        const details = this.props.getScreenDetails(props.scene);
        const {headerRight} = details.options;
        return headerRight || null;
    };

    /**
     * 渲染 header left 视图组件
     * @param props
     * @return {*}
     * @private
     */
    _renderLeft(props) {
        const {options} = this.props.getScreenDetails(props.scene);

        const {transitionPreset} = this.props;

        // On Android, or if we have a custom header left, or if we have a custom back image, we
        // do not use the modular header (which is the one that imitates UINavigationController)
        if (
            transitionPreset !== 'uikit' ||
            options.headerBackImage ||
            options.headerLeft ||
            options.headerLeft === null
        ) {
            return this._renderSubView(
                props,
                'left',
                this._renderLeftComponent,
                this.props.leftInterpolator
            );
        } else {
            return this._renderModularSubView(
                props,
                'left',
                this._renderModularLeftComponent,
                this.props.leftLabelInterpolator,
                this.props.leftButtonInterpolator
            );
        }
    }

    /**
     * 渲染 header title 视图组件
     * @param props
     * @param options
     * @return {*}
     * @private
     */
    _renderTitle(props, options) {
        const style = {};
        const {transitionPreset} = this.props;

        // 根据平台类型信息,HeaderLeft,HeaderRight组件是否存在等信息
        // 重新调整 HeaderTitle 容器组件的左右边距
        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;
        }

        return this._renderSubView(
            {...props, style},
            'title',
            this._renderTitleComponent,
            transitionPreset === 'uikit'
                ? this.props.titleFromLeftInterpolator
                : this.props.titleInterpolator
        );
    }

    /**
     * 渲染 header right 视图组件
     * @param props
     * @return {*}
     * @private
     */
    _renderRight(props) {
        return this._renderSubView(
            props,
            'right',
            this._renderRightComponent,
            this.props.rightInterpolator
        );
    }

    _renderModularSubView(
        props,
        name,
        renderer,
        labelStyleInterpolator,
        buttonStyleInterpolator
    ) {
        const {scene} = props;
        const {index, isStale, key} = scene;

        // Never render a modular back button on the first screen in a stack.
        if (index === 0) {
            return;
        }

        const offset = this.props.navigation.state.index - index;

        if (Math.abs(offset) > 2) {
            // Scene is far away from the active scene. Hides it to avoid unnecessary
            // rendering.
            return null;
        }

        const ButtonContainer = ({children}) => (
            <Animated.View
                style={[buttonStyleInterpolator({...this.props, ...props})]}
            >
                {children}
            </Animated.View>
        );

        const LabelContainer = ({children}) => (
            <Animated.View
                style={[labelStyleInterpolator({...this.props, ...props})]}
            >
                {children}
            </Animated.View>
        );

        const subView = renderer(props, ButtonContainer, LabelContainer);

        if (subView === null) {
            return subView;
        }

        const pointerEvents = offset !== 0 || isStale ? 'none' : 'box-none';

        return (
            <View
                key={`${name}_${key}`}
                pointerEvents={pointerEvents}
                style={[styles.item, styles[name], props.style]}
            >
                {subView}
            </View>
        );
    }

    _renderSubView(props, name, renderer, styleInterpolator) {
        const {scene} = props;
        const {index, isStale, key} = scene;

        const offset = this.props.navigation.state.index - index;

        if (Math.abs(offset) > 2) {
            // Scene is far away from the active scene. Hides it to avoid unnecessary
            // rendering.
            return null;
        }

        const subView = renderer(props);

        if (subView == null) {
            return null;
        }

        const pointerEvents = offset !== 0 || isStale ? 'none' : 'box-none';

        return (
            <Animated.View
                pointerEvents={pointerEvents}
                key={`${name}_${key}`}
                style={[
                    styles.item,
                    styles[name],
                    props.style,
                    styleInterpolator({
                        // todo: determine if we really need to splat all this.props
                        ...this.props,
                        ...props,
                    }),
                ]}
            >
                {subView}
            </Animated.View>
        );
    }

    _renderHeader(props) {
        // 渲染 HeaderLeft
        const left = this._renderLeft(props);
        // 渲染 HeaderRight
        const right = this._renderRight(props);
        // 渲染 HeaderTitle,第二个参数传递 HeaderLeft, HeaderRight 组件是否存在的信息
        const title = this._renderTitle(props, {
            hasLeftComponent: !!left,
            hasRightComponent: !!right,
        });

        const {isLandscape, transitionPreset} = this.props;
        const {options} = this.props.getScreenDetails(props.scene);

        // 构造头部组件的属性对象,包含 样式 style 和 key 属性
        const wrapperProps = {
            style: styles.header,
            key: `scene_${props.scene.key}`,
        };

        if (
            options.headerLeft ||
            options.headerBackImage ||
            Platform.OS !== 'ios' ||
            transitionPreset !== 'uikit'
        ) {
            // android/headerLeft被指定/headerBackImage被指定/transitionPreset不是uikit
            return (
                <View {...wrapperProps}>
                    {title}
                    {left}
                    {right}
                </View>
            );
        } else {
            // ios
            return (
                <MaskedViewIOS
                    {...wrapperProps}
                    maskElement={
                        <View style={styles.iconMaskContainer}>
                            <Image
                                source={require('../assets/back-icon-mask.png')}
                                style={styles.iconMask}
                            />
                            <View style={styles.iconMaskFillerRect}/>
                        </View>
                    }
                >
                    {title}
                    {left}
                    {right}
                </MaskedViewIOS>
            );
        }
    }

    render() {
        let appBar;
        const {mode, scene, isLandscape} = this.props;

        if (mode === 'float') {
            const scenesByIndex = {};
            this.props.scenes.forEach(scene => {
                scenesByIndex[scene.index] = scene;
            });
            const scenesProps = Object.values(scenesByIndex).map(scene => ({
                position: this.props.position,
                progress: this.props.progress,
                scene,
            }));
            // 对于 props.scenes 中的每个 scene 执行 _renderHeader,
            // appBar 会是一个数组
            appBar = scenesProps.map(this._renderHeader, this);
        } else {
            // 对于当前 props.scene 执行 _renderHeader,
            // appBar 会是一个对象
            appBar = this._renderHeader({
                position: new Animated.Value(this.props.scene.index),
                progress: new Animated.Value(0),
                scene: this.props.scene,
            });
        }

        const {options} = this.props.getScreenDetails(scene);
        // 获取导航器 navigationOptions 设置的 headerStyle
        const {headerStyle = {}} = options;
        const headerStyleObj = StyleSheet.flatten(headerStyle);
        const appBarHeight = getAppBarHeight(isLandscape);

        const {
            alignItems,
            justifyContent,
            flex,
            flexDirection,
            flexGrow,
            flexShrink,
            flexBasis,
            flexWrap,
            ...safeHeaderStyle
        } = headerStyleObj;

        if (__DEV__) {
            warnIfHeaderStyleDefined(alignItems, 'alignItems');
            warnIfHeaderStyleDefined(justifyContent, 'justifyContent');
            warnIfHeaderStyleDefined(flex, 'flex');
            warnIfHeaderStyleDefined(flexDirection, 'flexDirection');
            warnIfHeaderStyleDefined(flexGrow, 'flexGrow');
            warnIfHeaderStyleDefined(flexShrink, 'flexShrink');
            warnIfHeaderStyleDefined(flexBasis, 'flexBasis');
            warnIfHeaderStyleDefined(flexWrap, 'flexWrap');
        }

        // TODO: warn if any unsafe styles are provided
        const containerStyles = [
            options.headerTransparent
                ? styles.transparentContainer
                : styles.container,
            {height: appBarHeight},
            safeHeaderStyle,
        ];

        const {headerForceInset} = options;
        const forceInset = headerForceInset || {top: 'always', bottom: 'never'};

        return (
            <SafeAreaView forceInset={forceInset} style={containerStyles}>
                <View style={StyleSheet.absoluteFill}>{options.headerBackground}</View>
                <View style={{flex: 1}}>{appBar}</View>
            </SafeAreaView>
        );
    }
}

function warnIfHeaderStyleDefined(value, styleProp) {
    if (value !== undefined) {
        console.warn(
            `${styleProp} was given a value of ${value}, this has no effect on headerStyle.`
        );
    }
}

// platformContainerStyles 针对不同的平台对Header所在容器视图定义了缺省样式 :
// 1. ios : header view 底部边界线为一个像素宽的一条线,颜色为 #A7A7AA;
// 2. android : header view 底部边界线为一个阴影效果;
// 如果想禁用或者覆盖此缺省效果,在 navigationOptions.headerStyle 中将下面各个属性设置为 undefined
// 或者你想要的效果值
let platformContainerStyles;
if (Platform.OS === 'ios') {
    platformContainerStyles = {
        borderBottomWidth: StyleSheet.hairlineWidth,
        borderBottomColor: '#A7A7AA',
    };
} else {
    platformContainerStyles = {
        shadowColor: 'black',
        shadowOpacity: 0.1,
        shadowRadius: StyleSheet.hairlineWidth,
        shadowOffset: {
            height: StyleSheet.hairlineWidth,
        },
        elevation: 4,
    };
}

const styles = StyleSheet.create({
    container: {
        backgroundColor: Platform.OS === 'ios' ? '#F7F7F7' : '#FFF',
        ...platformContainerStyles,
    },
    transparentContainer: {
        position: 'absolute',
        top: 0,
        left: 0,
        right: 0,
        ...platformContainerStyles,
    },
    // Header 容器视图的缺省样式定义
    header: {
        ...StyleSheet.absoluteFillObject,
        flexDirection: 'row',
    },
    item: {
        backgroundColor: 'transparent',
    },
    iconMaskContainer: {
        flex: 1,
        flexDirection: 'row',
        justifyContent: 'center',
    },
    iconMaskFillerRect: {
        flex: 1,
        backgroundColor: '#d8d8d8',
        marginLeft: -3,
    },
    iconMask: {
        // These are mostly the same as the icon in ModularHeaderBackButton
        height: 21,
        width: 12,
        marginLeft: 9,
        marginTop: -0.5, // resizes down to 20.5
        alignSelf: 'center',
        resizeMode: 'contain',
    },
    // head title 视图的缺省样式: 上下边距都为0,左右边距都为TITLE_OFFSET,绝对定位
    // 相当于放到 head 视图水平方向的中间,上下贴边父容器
    // 内部元素行式布局,垂直方向居中
    title: {
        bottom: 0,
        top: 0,
        left: TITLE_OFFSET,
        right: TITLE_OFFSET,
        position: 'absolute',
        alignItems: 'center',
        flexDirection: 'row',
        justifyContent: Platform.OS === 'ios' ? 'center' : 'flex-start',
    },

    // head left 视图的缺省样式 : 左边贴边,上下贴边,绝对定位
    // 相当于放到 head 视图水平方向的最左边,上下贴边父容器
    // 内部元素行式布局,垂直方向居中
    left: {
        left: 0,
        bottom: 0,
        top: 0,
        position: 'absolute',
        alignItems: 'center',
        flexDirection: 'row',
    },
    // head left 视图的缺省样式 : 右边贴边,上下贴边,绝对定位
    // 相当于放到 head 视图水平方向的最右边,上下贴边父容器
    // 内部元素行式布局,垂直方向居中
    right: {
        right: 0,
        bottom: 0,
        top: 0,
        position: 'absolute',
        flexDirection: 'row',
        alignItems: 'center',
    },
});

// 使用 HOC withOrientation 增强封装上面定义的 Header 组件,使其带有一个状态 isLandscape (横屏状态),
// 在屏幕方向发生变化时此状态会被修改
export default withOrientation(Header);

猜你喜欢

转载自blog.csdn.net/andy_zhang2007/article/details/80335041