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

import React from 'react';

import clamp from 'clamp';
import {Animated, Easing, I18nManager, PanResponder, Platform, StyleSheet, View,} 
       from 'react-native';

import Card from './Card';
import Header from '../Header/Header';
import NavigationActions from '../../NavigationActions';
import addNavigationHelpers from '../../addNavigationHelpers';
import getChildEventSubscriber from '../../getChildEventSubscriber';
import SceneView from '../SceneView';

import TransitionConfigs from './TransitionConfigs';
import * as ReactNativeFeatures from '../../utils/ReactNativeFeatures';

const emptyFunction = () => {
};

const EaseInOut = Easing.inOut(Easing.ease);

/**
 * 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
 * is always less then the full distance of the layout.
 */
const ANIMATION_DURATION = 500;

/**
 * The gesture distance threshold to trigger the back behavior. For instance,
 * `1/2` means that moving greater than 1/2 of the width of the screen will
 * trigger a back action
 */
const POSITION_THRESHOLD = 1 / 2;

/**
 * The threshold (in pixels) to start the gesture action.
 * 拖拽距离超过此值,手势成立,才进行相应的处理
 */
const RESPOND_THRESHOLD = 20;

/**
 * The distance of touch start from the edge of the screen where the gesture will be recognized
 */
// 缺省情况下,水平方向上,拖拽起始点<25,拖拽手势被认可
const GESTURE_RESPONSE_DISTANCE_HORIZONTAL = 25;
// 缺省情况下,竖直方向上,拖拽起始点坐标<135,拖拽手势被认可
const GESTURE_RESPONSE_DISTANCE_VERTICAL = 135;

const animatedSubscribeValue = animatedValue => {
    if (!animatedValue.__isNative) {
        return;
    }
    if (Object.keys(animatedValue._listeners).length === 0) {
        animatedValue.addListener(emptyFunction);
    }
};

/**
 * Card Stack, 卡片堆栈,
 * 1. 对于一个卡片 Card , 你可以把它理解成正好适合屏幕大小一样的一张卡片;
 * 2. 每张卡片用于渲染某一个开发人员提供的路由场景屏幕组件(也就是对应到路由设置中的一项)和头部;
 * 3. 同一张卡片上不会同时出现两个路由场景屏幕的内容;
 * 4. Card Stack 会把所有的这些卡片渲染出来,渲染过程是根据卡片相应场景屏幕在导航路由栈中的 index
 *   从小到大的顺序在 z 轴上渲染出每张卡片。因此你可以把一个 Card Stack 想象成一个在垂直于手机屏幕
 *   平面方向上堆叠起来的一沓名片,用户早期访问的卡片距离客户较远,用户最近访问的卡片距离客户更近。
 * 5. Card Stack 同时也负责了手势识别关闭当前屏幕的工作 :
 *  - 1. 如果 navigationOptions  中指定了 boolean 类型的 gesturesEnabled, 根据它的值决定是否支持手势关屏,
 *  - 2. 否则如果没有指定这样一个选项,但平台是 iOS, 则支持手势关屏 ,
 *  - 3. 否则,也就是平台是 Android, 则不支持手势关屏.
 */
class CardStack extends React.Component {
    /**
     * Used to identify the starting point of the position when the gesture starts, such that it 
     * can be updated according to its relative position. This means that a card can effectively 
     * be "caught"- If a gesture starts while a card is animating, the card does not jump into a
     * corresponding location for the touch.
     */
    _gestureStartValue = 0;

    // tracks if a touch is currently happening
    _isResponding = false;

    /**
     * immediateIndex is used to represent the expected index that we will be on after a
     * transition. To achieve a smooth animation when swiping back, the action to go back
     * doesn't actually fire until the transition completes. The immediateIndex is used during
     * the transition so that gestures can be handled correctly. This is a work-around for
     * cases when the user quickly swipes back several times.
     *
     * _immediateIndex 用于记录动画结束后的目标场景屏幕 index。为了滑动返回动画能够平滑进行,
     * 返回所对应的 action 会在动画结束时才派发去改变导航状态。在此过程中,导航状态并不发生变化,
     * _immediateIndex 也不变,用来确保手势能得到正确地处理。
     *
     * 这是一个用户快速多次滑动返回时的 work-around (应急解决方案)。
     */
    _immediateIndex = null;

    /**
     * 保存每个场景的屏幕详情信息,可以理解成是一个Map,
     * key 是 scene.key, value 是一个 screenDetails 对象,
     * 关于 screenDetails 对象,可以参考函数实现 :  this._getScreenDetails(scene) ,
     *
     * @type {{}}
     * @private
     */
    _screenDetails = {};

    _childEventSubscribers = {};


    componentWillReceiveProps(props) {
        if (props.screenProps !== this.props.screenProps) {
            this._screenDetails = {};
        }
        props.transitionProps.scenes.forEach(newScene => {
            if (
                this._screenDetails[newScene.key] &&
                this._screenDetails[newScene.key].state !== newScene.route
            ) {
                this._screenDetails[newScene.key] = null;
            }
        });
    }

    componentDidUpdate() {
        const activeKeys = this.props.transitionProps.navigation.state.routes.map(
            route => route.key
        );
        Object.keys(this._childEventSubscribers).forEach(key => {
            if (!activeKeys.includes(key)) {
                delete this._childEventSubscribers[key];
            }
        });
    }

    /**
     * 判断指定路由 route 是否当前焦点 (当前活跃,用户当前正在操作的屏幕, 这里都可认为是相同语义)
     * @param route
     * @return {boolean}
     * @private
     */
    _isRouteFocused = route => {
        const {transitionProps: {navigation: {state}}} = this.props;
        const focusedRoute = state.routes[state.index];
        return route === focusedRoute;
    };

    /**
     * 获取指定场景 scene 的屏幕详情信息对象
     *
     *    screenDetails 对象的一个例子 :
     * @param scene
     * @return {*}
     * @private
     */
    _getScreenDetails = scene => {
        const {screenProps, transitionProps: {navigation}, router} = this.props;
        let screenDetails = this._screenDetails[scene.key];
        if (!screenDetails || screenDetails.state !== scene.route) {
            if (!this._childEventSubscribers[scene.route.key]) {
                this._childEventSubscribers[scene.route.key] = getChildEventSubscriber(
                    navigation.addListener,
                    scene.route.key
                );
            }

            const screenNavigation = addNavigationHelpers({
                dispatch: navigation.dispatch,
                state: scene.route,
                isFocused: () => this._isRouteFocused(scene.route),
                addListener: this._childEventSubscribers[scene.route.key],
            });
            screenDetails = {
                state: scene.route,
                navigation: screenNavigation,
                options: router.getScreenOptions(screenNavigation, screenProps),
            };
            this._screenDetails[scene.key] = screenDetails;
        }
        return screenDetails;
    };

    /**
     * 渲染屏幕头部组件
     * @param scene
     * @param headerMode
     * @return {*}
     * @private
     */
    _renderHeader(scene, headerMode) {
        // 获取为当前场景指定的头部组件 :
        // 1.可能是一个一般组件 : 头部组件,可以直接返回使用 ;
        // 2.也可能是个函数 : 头部渲染函数 ;
        const {header} = this._getScreenDetails(scene).options;

        if (typeof header !== 'undefined' && typeof header !== 'function') {
            // 外部指定了自定义头部组件,并且该头部组件不是一个函数
            return header;
        }

        // 准备真正要使用的头部渲染函数 renderHeader
        const renderHeader = header || (props => <Header {...props} />);
        const {
            headerLeftInterpolator,
            headerTitleInterpolator,
            headerRightInterpolator,
        } = this._getTransitionConfig();

        const {
            mode,
            transitionProps,
            prevTransitionProps,
            ...passProps
        } = this.props;

        return renderHeader({
            ...passProps,
            ...transitionProps,
            scene,
            mode: headerMode,
            transitionPreset: this._getHeaderTransitionPreset(),
            getScreenDetails: this._getScreenDetails,
            leftInterpolator: headerLeftInterpolator,
            titleInterpolator: headerTitleInterpolator,
            rightInterpolator: headerRightInterpolator,
        });
    }

    // eslint-disable-next-line class-methods-use-this
    _animatedSubscribe(props) {
        // Hack to make this work with native driven animations. We add a single listener
        // so the JS value of the following animated values gets updated. We rely on
        // some Animated private APIs and not doing so would require using a bunch of
        // value listeners but we'd have to remove them to not leak and I'm not sure
        // when we'd do that with the current structure we have. `stopAnimation` callback
        // is also broken with native animated values that have no listeners so if we
        // want to remove this we have to fix this too.
        animatedSubscribeValue(props.transitionProps.layout.width);
        animatedSubscribeValue(props.transitionProps.layout.height);
        animatedSubscribeValue(props.transitionProps.position);
    }

    /**
     * 复位到指定索引 resetToIndex 的卡片 , 有动画,动画效果和 _goBack 类似
     * 1.Android 使用 Animated.timing + EaseInOut
     * 2.iOS 使用 Animated.spring
     * @param resetToIndex 目标卡片的索引
     * @param duration 动画时间 , 仅在 Android上有效, iOS上使用了 spring, 不使用此参数
     * @private
     */
    _reset(resetToIndex, duration) {
        if (
            Platform.OS === 'ios' &&
            ReactNativeFeatures.supportsImprovedSpringAnimation()
        ) {
            Animated.spring(this.props.transitionProps.position, {
                toValue: resetToIndex,
                stiffness: 5000,
                damping: 600,
                mass: 3,
                useNativeDriver: this.props.transitionProps.position.__isNative,
            }).start();
        } else {
            Animated.timing(this.props.transitionProps.position, {
                toValue: resetToIndex,
                duration,
                easing: EaseInOut,
                useNativeDriver: this.props.transitionProps.position.__isNative,
            }).start();
        }
    }

    /**
     * 从指定索引的场景屏幕返回 , 有动画,动画效果和 _reset 类似
     * 1.Android 使用 Animated.timing + EaseInOut
     * 2.iOS 使用 Animated.spring
     * @param backFromIndex 返回动作开始时的场景屏幕的索引
     * @param duration 动画时间 , 仅在 Android上有效, iOS上使用了 spring, 不使用此参数
     * @private
     */
    _goBack(backFromIndex, duration) {
        const {navigation, position, scenes} = this.props.transitionProps;
        // 用于记录目标场景屏幕的索引
        const toValue = Math.max(backFromIndex - 1, 0);

        // set temporary index for gesture handler to respect until the action is
        // dispatched at the end of the transition.
        // 目标场景屏幕的索引记录到当前实例属性 _immediateIndex 上
        this._immediateIndex = toValue;

        // 下面代码定义了返回动画和动画完成时的回调函数,然后启动了动画,
        // 需要注意 :
        // 1. 动画过程中 this._immediateIndex 保持不变,指向返回的目标场景屏幕;
        // 2. 动画结束时 this._immediateIndex 复位为 null;
        // 3. 动画结束时,才真正派发 BACK action 去改变路由导航状态;

        // 动画结束时的回调函数
        const onCompleteAnimation = () => {
            this._immediateIndex = null;

            // 此时此刻,界面展现的场景屏幕已经是屏幕返回动作的目标场景屏幕,
            // 但是路由导航状态中当前场景屏幕仍然是返回动作的起始场景屏幕,
            // 需要向 navigation 派发 BACK action 从而消除这一不一致现象
            const backFromScene = scenes.find(s => s.index === toValue + 1);
            if (!this._isResponding && backFromScene) {
                // 如果现在没有在响应手势,并且找到了返回动作起始场景屏幕,
                // 则派发从该场景屏幕开始的一个 BACK action, 对路由导航状态
                // 进行相应的更新,从而保持界面展现和路由导航状态对象的一致
                navigation.dispatch(
                    NavigationActions.back({
                        key: backFromScene.route.key,
                        immediate: true,
                    })
                );
            }
        };


        // 开始动画
        if (
            Platform.OS === 'ios' &&
            ReactNativeFeatures.supportsImprovedSpringAnimation()
        ) {
            Animated.spring(position, {
                toValue,
                stiffness: 5000,
                damping: 600,
                mass: 3,
                useNativeDriver: position.__isNative,
            }).start(onCompleteAnimation);
        } else {
            Animated.timing(position, {
                toValue,
                duration,
                easing: EaseInOut,
                useNativeDriver: position.__isNative,
            }).start(onCompleteAnimation);
        }
    }

    /**
     * 渲染 CarkStack,
     * 元素层次结构 :
     *  View (flex:1,column-reverse):
     *      View (flex:1): (假设有 N 个scene,则会有 N 张Card)
     *              Card 0
     *              Card 1
     *              Card ...
     *              Card n-1
     *      FloatingHeader (仅在headerMode为float时出现,参考 this._renderHeader)
     * @return {*}
     */
    render() {
        // 这里对 float 头部进行渲染处理
        // screen 的头部不在这里处理,是在 this._renderCard -> this._renderInnerScene 中进行处理
        let floatingHeader = null;
        const headerMode = this._getHeaderMode();
        if (headerMode === 'float') {
            floatingHeader = this._renderHeader(
                this.props.transitionProps.scene,
                headerMode
            );
        }

        // 获取屏幕切换过渡动画属性 transitionProps 和屏幕模式 mode
        const {
            transitionProps: {navigation, position, layout, scene, scenes},
            mode,
        } = this.props;

        // 获取当前路由/屏幕/场景的索引
        const {index} = navigation.state;
        // 如果当前屏幕模式为 modal, 则将会处理手势竖直方向(y)变化,否则会处理水平方向(x)变化
        const isVertical = mode === 'modal';
        // 获取 navigationOptions,该信息是导航器上的 navigationOptions 和 屏幕组件中的 
        // navigationOptions 的合并(后者优先级更高)
        const {options} = this._getScreenDetails(scene);
        // 手势方向是否要逆转
        // 缺省情况下,是不逆转,意味着:
        // 1. x 轴上,向左滑动,滑动距离(gesture['dx'])是负值;向右滑动,滑动距离(gesture['dx'])是正值;
        // 2. y 轴上,向上滑动,滑动距离(gesture['dy'])是负值;向下滑动,滑动距离(gesture['dy'])是正值;
        const gestureDirectionInverted = options.gestureDirection === 'inverted';

        // 检查是否开启了手势 gesturesEnabled
        // 1. 如果 navigationOptions  中指定了 boolean 类型的 gesturesEnabled, 
        就使用它作为 gesturesEnabled ,
        // 2. 否则如果没有指定这样一个选项,但平台是 iOS, 则 gesturesEnabled 使用 true ,
        // 3. 否则,也就是平台是 Android, gesturesEnabled 使用 false .
        const gesturesEnabled =
            typeof options.gesturesEnabled === 'boolean'
                ? options.gesturesEnabled
                : Platform.OS === 'ios';

        // 构造手势处理器 responder
        // 如果 gesturesEnabled 为 false, responder 指定为 null,表示不处理手势,
        const responder = !gesturesEnabled
            ? null
            : PanResponder.create({
                // 关于 PanResponder 全面介绍,可以参考官方文档 :
                // https://facebook.github.io/react-native/docs/panresponder.html
                // 另一个组件已经成为了新的响应者,当前手势被取消时的处理逻辑
                onPanResponderTerminate: () => {
                    // 相应手势标记设置为 false, 表示不在响应手势过程中了
                    this._isResponding = false;
                    // 立即返回到手势开始时那个场景屏幕
                    this._reset(index, 0);
                },
                // 一旦成为触摸事件响应者时的处理逻辑
                onPanResponderGrant: () => {
                    // 停止目前进行中的 postion 动画,并
                    // 1. 标记响应手势事件开始 _isResponding
                    // 2. 记录手势事件处理开始时的起始信息  _gestureStartValue
                    position.stopAnimation(value => {
                        this._isResponding = true;
                        this._gestureStartValue = value;
                    });
                },
                /**
                 * 触摸进行过程中愿不愿意成为响应者
                 * @param event
                 * @param gesture
                 * @return {boolean}
                 */
                onMoveShouldSetPanResponder: (event, gesture) => {
                    if (index !== scene.index) {
                        // 如果要渲染的当前 scene 和 导航路由状态中的当前路由 index 不一致,
                        // 则返回 false 表明不愿意响应该手势
                        return false;
                    }

                    // 愿意成为手势响应者

                    // 触摸移动过程中获取当前操作的场景屏幕组件的 index
                    const immediateIndex =
                        this._immediateIndex == null ? index : this._immediateIndex;
                    // 当前拖拽距离
                    const currentDragDistance = gesture[isVertical ? 'dy' : 'dx'];
                    // 当前拖拽位置
                    const currentDragPosition =
                        event.nativeEvent[isVertical ? 'pageY' : 'pageX'];
                    // 获取拖拽轴方向屏幕的长度
                    const axisLength = isVertical
                        ? layout.height.__getValue()
                        : layout.width.__getValue();
                    const axisHasBeenMeasured = !!axisLength;

                    // Measure the distance from the touch to the edge of the screen
                    // 测量手势开始时的触碰位置距离屏幕边缘的距离(使用那一点的x/y轴坐标表示)
                    const screenEdgeDistance = gestureDirectionInverted
                        ? axisLength - (currentDragPosition - currentDragDistance)
                        : currentDragPosition - currentDragDistance;
                    // Compare to the gesture distance relavant to card or modal
                    // 获取用户通过 navigationOptions 自定义的手势识别距离
                    const {
                        gestureResponseDistance: userGestureResponseDistance = {},
                    } = this._getScreenDetails(scene).options;

                    // 确定最终要在手势识别轴上使用的手势识别距离
                    const gestureResponseDistance = isVertical
                        ? userGestureResponseDistance.vertical ||
                        GESTURE_RESPONSE_DISTANCE_VERTICAL
                        : userGestureResponseDistance.horizontal ||
                        GESTURE_RESPONSE_DISTANCE_HORIZONTAL;

                    // GESTURE_RESPONSE_DISTANCE is about 25 or 30. Or 135 for modals
                    if (screenEdgeDistance > gestureResponseDistance) {
                        // Reject touches that started in the middle of the screen
                        // 仅接受触摸起始位置在gestureResponseDistance以内的手势
                        return false;
                    }

                    // 拖拽距离足够大了吗 ?
                    const hasDraggedEnough =
                        Math.abs(currentDragDistance) > RESPOND_THRESHOLD;

                    // 是否在卡片栈中最下面一张卡片上 ?
                    const isOnFirstCard = immediateIndex === 0;
                    // 是否要执行响应逻辑 ?
                    const shouldSetResponder =
                        hasDraggedEnough && axisHasBeenMeasured && !isOnFirstCard;
                    return shouldSetResponder;
                },

                // 手势响应过程中当前卡片跟随触摸移动
                onPanResponderMove: (event, gesture) => {
                    // Handle the moving touches for our granted responder
                    const startValue = this._gestureStartValue;
                    const axis = isVertical ? 'dy' : 'dx';
                    const axisDistance = isVertical
                        ? layout.height.__getValue()
                        : layout.width.__getValue();
                    const currentValue =
                        (I18nManager.isRTL && axis === 'dx') !== gestureDirectionInverted
                            ? startValue + gesture[axis] / axisDistance
                            : startValue - gesture[axis] / axisDistance;
                    // 计算目标位置 position ,一定要介于[currentValue,index] 之间的某个小数
                    // 缺省情况下,比如是正在处理水平方向上的滑动,越靠近屏幕左边, position
                    // 越小,也就是从屏幕左边向右边的整个滑动过程中, position 是从 index 开始逐渐
                    // 减小,在贴近屏幕右边缘的时候逼近 index -1, 这里对应的设计是让自左向右滑动
                    // 关闭当前屏幕,返回上一屏幕
                    const value = clamp(index - 1, currentValue, index);
                    position.setValue(value);
                },
                onPanResponderTerminationRequest: () =>
                    // Returning false will prevent other views from becoming responder while
                    // the navigation view is the responder (mid-gesture)
                    false,

                // 当前视图正在处理的手势操作已经完成
                onPanResponderRelease: (event, gesture) => {
                    if (!this._isResponding) {
                        // 怎么会走到这里 ? 一定是出了什么问题。这里什么都不做。
                        return;
                    }

                    // 应该总是走到这里

                    // 复位手势响应中标志为 false
                    this._isResponding = false;

                    // 触摸移动结束时获取当前操作的场景屏幕组件的 index
                    const immediateIndex =
                        this._immediateIndex == null ? index : this._immediateIndex;

                    // Calculate animate duration according to gesture speed and moved distance
                    //  手势轴方向屏幕尺寸
                    const axisDistance = isVertical
                        ? layout.height.__getValue()
                        : layout.width.__getValue();
                    // 运动方向
                    const movementDirection = gestureDirectionInverted ? -1 : 1;
                    // 手势轴方向手势移动距离
                    const movedDistance =
                        movementDirection * gesture[isVertical ? 'dy' : 'dx'];
                    // 手势轴方向手势运动速度
                    const gestureVelocity =
                        movementDirection * gesture[isVertical ? 'vy' : 'vx'];
                    //  缺省速度
                    const defaultVelocity = axisDistance / ANIMATION_DURATION;
                    // 用于计算reset或者goBack切换动画需要的时间的速度
                    const velocity = Math.max(
                        Math.abs(gestureVelocity),
                        defaultVelocity
                    );

                    // 手势结束时,不管是要切换屏幕还是复位到原来的屏幕,
                    // 都计算出来相应的需要的时间用于稍后播放动画需要
                    const resetDuration = gestureDirectionInverted
                        ? (axisDistance - movedDistance) / velocity
                        : movedDistance / velocity;
                    const goBackDuration = gestureDirectionInverted
                        ? movedDistance / velocity
                        : (axisDistance - movedDistance) / velocity;

                    // To asyncronously get the current animated value, we need to run 
                    // stopAnimation:
                    // 手势滑动动作结束时,停止当前 position 动画,并根据手势速度或者手势移动距离决定
                    // 是复位到手势开始时屏(_reset),还是返回当前屏幕之前的屏幕(_goBack)
                    position.stopAnimation(value => {
                        // If the speed of the gesture release is significant, use that as 
                        // the indication of intent
                        if (gestureVelocity < -0.5) {
                            this._reset(immediateIndex, resetDuration);
                            return;
                        }
                        if (gestureVelocity > 0.5) {
                            this._goBack(immediateIndex, goBackDuration);
                            return;
                        }

                        // Then filter based on the distance the screen was moved. Over a third 
                        // of the way swiped, and the back will happen.
                        // 1. 缺省情况下,比如是正在处理水平方向上的滑动,越靠近屏幕左边, position
                        // 越小,也就是从屏幕左边向右边的整个滑动过程中, position 是从 index 开始逐渐
                        // 减小,在贴近屏幕右边缘的时候逼近 index -1, 这里对应的设计是让自左向右滑动
                        // 关闭当前屏幕,返回上一屏幕
                        // 2. POSITION_THRESHOLD  位置切屏参考值常量设计为 0.5 (对应屏幕正中间竖直线为
                        // 边界),
                        // 基于以上逻辑实现如下 :
                        // 1. 如果拖拽到屏幕右半边,则关闭当前屏幕返回上一屏幕;
                        // 2. 如果拖拽到屏幕左半边,则回复到手势开始时的屏幕;
                        if (value <= index - POSITION_THRESHOLD) {
                            // 1. 如果拖拽到屏幕右半边,则关闭当前屏幕返回上一屏幕;
                            this._goBack(immediateIndex, goBackDuration);
                        } else {
                            // 2. 如果拖拽到屏幕左半边,则回复到手势开始时的屏幕;
                            this._reset(immediateIndex, resetDuration);
                        }
                    });
                },
            });

        // 决定最重要是用的
        const handlers = gesturesEnabled ? responder.panHandlers : {};
        // 构建容器使用的样式:缺省定义 + 自定义 transitionConfig.containerStyle
        const containerStyle = [
            styles.container,
            this._getTransitionConfig().containerStyle,
        ];

        // 注意 :
        // 下面的代码是将 CardStack 中所有的 Card 按照 index 从小到大的顺序都渲染了出来,
        // 但是由于 Card 本身的样式定义是绝对定位,所以它们是一张张在同一位置叠放在一起的
        return (
            <View {...handlers} style={containerStyle}>
                <View style={styles.scenes}>
                    {scenes.map(s => this._renderCard(s))}
                </View>
                {floatingHeader}
            </View>
        );
    }

    /**
     * 获取头部模式 headerMode :
     * 1. 如果组件属性中指定了头部模式 headerMode,直接使用 ;
     * 2. 否则 ,
     * 2.1 如果是 Android, 或者 iOS + modal 屏幕模式, 返回 screen ;
     * 2.2 其他情况 (iOS + card 屏幕模式),返回 float ;
     * @return {string}
     * @private
     */
    _getHeaderMode() {
        if (this.props.headerMode) {
            return this.props.headerMode;
        }
        if (Platform.OS === 'android' || this.props.mode === 'modal') {
            return 'screen';
        }
        return 'float';
    }

    /**
     * 获取头部过渡预定义样式
     * 1. Android, 或者 iOS + screen 头部模式, 返回 fade-in-place ;
     * 2. 否则, 如果组件属性指定了 headerTransitionPreset, 使用它 ;
     * 3. 否则, 使用 fade-in-place .
     * @return {string}
     * @private
     */
    _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)
        if (Platform.OS === 'android' || this._getHeaderMode() === 'screen') {
            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';
        }
    }

    /**
     * 在某个卡片 Card 渲染时,渲染该卡片上的场景屏幕组件,
     * 元素层次结构 :
     * 1. headerMode 是 screen 的情况
     * View (column-reverse):
     *   View (flex:1):
     *       SceneView :
     *           SceneComponent (navigation)
     *   Header
     * 2. headerMode 不是 screen 的情况(比如 float)
     * SceneView :
     *     SceneComponent (navigation)
     *
     * @param SceneComponent 路由对应的场景屏幕组件
     * @param scene
     * @return {*}
     * @private
     */
    _renderInnerScene(SceneComponent, scene) {
        const {navigation} = this._getScreenDetails(scene);
        const {screenProps} = this.props;
        const headerMode = this._getHeaderMode();
        if (headerMode === 'screen') {
            return (
                <View style={styles.container}>
                    <View style={{flex: 1}}>
                        <SceneView
                            screenProps={screenProps}
                            navigation={navigation}
                            component={SceneComponent}
                        />
                    </View>
                    {this._renderHeader(scene, headerMode)}
                </View>
            );
        }
        return (
            <SceneView
                screenProps={this.props.screenProps}
                navigation={navigation}
                component={SceneComponent}
            />
        );
    }

    /**
     * 根据当前组件属性中的信息计算出屏幕场景切换过渡动画设置对象,
     * 所基于的信息 :
     * 1. 屏幕模式 mode : isModal , true ==> modal , false ==> card
     * 2. 自定义的过渡设置函数 : this.props.transitionConfig , 可以不提供
     * 3. 目标屏幕的过渡属性 : this.props.transitionProps
     * 4. 起始屏幕的过渡属性 : this.props.prevTransitionProps
     * @return {{}}
     * @private
     */
    _getTransitionConfig = () => {
        const isModal = this.props.mode === 'modal';

        return TransitionConfigs.getTransitionConfig(
            this.props.transitionConfig,
            this.props.transitionProps,
            this.props.prevTransitionProps,
            isModal
        );
    };

    /**
     * 渲染针对某个场景 scene 的屏幕组件的卡片组件 card ,
     * 元素层次结构 :
     *  Card :
     *      InnerScene (参考 this._renderInnerScene())
     * @param scene
     * @return {*}
     * @private
     */
    _renderCard = scene => {
        // 从屏幕切换过渡动画设置中获取屏幕差值样式定义函数 screenInterpolator
        const {screenInterpolator} = this._getTransitionConfig();
        // 构造将要使用的屏幕差值样式 style
        const style =
            screenInterpolator &&
            screenInterpolator({...this.props.transitionProps, scene});

        // 获取当前场景的路由对应的场景组件,记录到 SceneComponent
        const SceneComponent = this.props.router.getComponentForRouteName(
            scene.route.routeName
        );

        const {transitionProps, ...props} = this.props;

        // 注意 :
        // 下面渲染 Card 时候,将传递外部指定属性 this.props.cardStyle 到 Card 的属性 style ;
        return (
            <Card
                {...props}
                {...transitionProps}
                key={`card_${scene.key}`}
                style={[style, this.props.cardStyle]}
                scene={scene}
            >
                {this._renderInnerScene(SceneComponent, scene)}
            </Card>
        );
    };
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        // Header is physically rendered after scenes so that Header won't be
        // covered by the shadows of the scenes.
        // That said, we'd have use `flexDirection: 'column-reverse'` to move
        // Header above the scenes.
        flexDirection: 'column-reverse',
    },
    scenes: {
        flex: 1,
    },
});

export default CardStack;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值