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

import React from 'react';
import {Animated, Easing, StyleSheet, View} from 'react-native';
import invariant from '../utils/invariant';

import NavigationScenesReducer from './ScenesReducer';

// Used for all animations unless overriden
// 缺省屏幕过渡动画设置,可以被覆盖
const DefaultTransitionSpec = {
    duration: 250, // 250毫秒
    easing: Easing.inOut(Easing.ease),
    timing: Animated.timing,
};

/**
 * 屏幕切换过渡器, transitioner,
 * 目前仅被 CardStackTransitioner 使用,也就是在 StackNavigator 中使用,
 * 用于屏幕切换时的动画过渡控制
 */
class Transitioner extends React.Component {
    constructor(props, context) {
        super(props, context);

        // The initial layout isn't measured. Measured layout will be only available
        // when the component is mounted.
        // 缺省布局,都初始化为0,真正可用的布局属性在本组件的根View的onLayout回调中
        // 才计算出来
        const layout = {
            height: new Animated.Value(0),
            initHeight: 0,
            initWidth: 0,
            isMeasured: false, // 需要经过布局计算后才能设置为 true
            width: new Animated.Value(0),
        };

        // this.state 初始化,屏幕切换过渡动画属性对象
        this.state = {
            layout,
            position: new Animated.Value(this.props.navigation.state.index), // 初始化为当前屏幕的索引
            progress: new Animated.Value(1), // 动画进度,此初始值1并没用,每次动画开始时总是复位为0
            scenes: NavigationScenesReducer([], this.props.navigation.state),
        };

        this._prevTransitionProps = null;
        this._transitionProps = buildTransitionProps(props, this.state);
        this._isMounted = false;
        this._isTransitionRunning = false; // 用于标记是否处于动画过程中
        this._queuedTransition = null; // 用于缓存一次动画过程中到达的最后一个场景切换动画请求
    }

    componentWillMount() {
        this._onLayout = this._onLayout.bind(this);
        this._onTransitionEnd = this._onTransitionEnd.bind(this);
    }

    componentDidMount() {
        this._isMounted = true;
    }

    componentWillUnmount() {
        this._isMounted = false;
    }

    /**
     * 当组件属性变化时,看情况执行过渡动画或者不执行过渡动画
     * @param nextProps
     */
    componentWillReceiveProps(nextProps) {
        // 基于 当前场景数组,当前导航状态,目标导航状态,计算新的场景数组
        const nextScenes = NavigationScenesReducer(
            this.state.scenes,
            nextProps.navigation.state,
            this.props.navigation.state
        );

        // 如果新旧场景数组一样,说明一切如旧没有变化,所以不需要屏幕切换,直接返回
        if (nextScenes === this.state.scenes) {
            return;
        }

        // 判断是否发生了场景切换
        const indexHasChanged =
            nextProps.navigation.state.index !== this.props.navigation.state.index;
        if (this._isTransitionRunning) {
            // 属性变化导致了场景切换,但是发现已经在播放场景切换动画过程中,此时先把新的
            // 场景切换请求暂存到 this._queuedTransition (其实不是一个队列,而是只缓存
            // 一个,换句话讲,如果有多个过来,最后一个会被保留,其他会被丢弃),在当前执行
            // 的动画结束时会执行缓存的场景切换请求
            this._queuedTransition = {nextProps, nextScenes, indexHasChanged};
            return;
        }

        // 开始播放场景切换动画
        this._startTransition(nextProps, nextScenes, indexHasChanged);
    }

    _startTransition(nextProps, nextScenes, indexHasChanged) {
        // 基于当前状态和新的场景数组构造新的过渡状态
        const nextState = {
            ...this.state,
            scenes: nextScenes,
        };
        // 现在 nextState 中 :
        // 1. position 指向当先 scene , 是 上一个场景
        // 2. scenes 中, isActive === true 的 scene 已经是目标 scene , 是下一个场景

        // 获取 position, progress
        const {position, progress} = nextState;
        // 目前 position 应该是指向屏幕切换过渡前的那个场景
        // 明确复位 progess 为 0 (不关心它之前是什么值)
        progress.setValue(0);

        this._prevTransitionProps = this._transitionProps;
        this._transitionProps = buildTransitionProps(nextProps, nextState);

        // get the transition spec.
        // 看看有没有用户指定的场景切换动画设置,有的话应用之
        const transitionUserSpec = nextProps.configureTransition
            ? nextProps.configureTransition(
                this._transitionProps,
                this._prevTransitionProps
            )
            : null;

        const transitionSpec = {
            ...DefaultTransitionSpec,
            ...transitionUserSpec,
        };

        const {timing} = transitionSpec;
        delete transitionSpec.timing;

        const toValue = nextProps.navigation.state.index;
        const positionHasChanged = position.__getValue() !== toValue;

        // if swiped back, indexHasChanged == true && positionHasChanged == false
        // 定义动画 :
        // 1. progress 要从 0 过渡到 1,
        // 2. position 要从当前屏幕索引 过渡到 nextProps.navigation.state.index 指定的新屏幕的索引
        // 仅在 indexHasChanged && positionHasChanged 时才会有真正的过渡动画执行,
        // 其他情况下实际要执行的动画数组为空 [],也就是没有动画需要执行
        const animations =
            indexHasChanged && positionHasChanged
                ? [
                    timing(progress, {
                        ...transitionSpec,
                        toValue: 1,
                    }),
                    timing(position, {
                        ...transitionSpec,
                        toValue: nextProps.navigation.state.index,
                    }),
                ]
                : [];

        // update scenes and play the transition
        // 标记开始场景切换动画过渡
        this._isTransitionRunning = true;
        // 设置 this.state 为新的状态 nextState, 并在状态设置完成时,场景切换动画开始前,
        // 调用外部指定的场景切换动画开始回调函数 onTransitionStart, 如果该回调函数是一个
        // Promise, 等到该 Promise 执行结果返回后才开始动画
        this.setState(nextState, async () => {
            if (nextProps.onTransitionStart) {
                const result = nextProps.onTransitionStart(
                    this._transitionProps,
                    this._prevTransitionProps
                );

                if (result instanceof Promise) {
                    await result;
                }
            }
            // 开始动画,并在动画结束时调用场景切换过渡动画结束回调函数
            // this._onTransitionEnd
            // 注意 : 这里 animations 可能为空 [], 表示没有动画需要执行,
            // 但是不管有没有动画真正被执行,该函数的逻辑表明,当前组件
            // 属性变化时, _onTransitionStart/_onTransitionEnd 都会被执行
            Animated.parallel(animations).start(this._onTransitionEnd);
        });
    }

    /**
     * 渲染函数
     * this.setState()会触发该渲染函数被调用,但是其根View 的 onLayout 回调并不总是被调用,
     * onLayout 被调用的时机是该组件的根 View 需要被重新布局时(坐标,或者尺寸发生了变化,一般
     * 由父容器的某些因素导致)
     * @return {*}
     */
    render() {
        return (
            <View onLayout={this._onLayout} style={[styles.main]}>
                {this.props.render(this._transitionProps, this._prevTransitionProps)}
            </View>
        );
    }

    /**
     * 该组件的根View 的 onLayout 回调函数
     * 当该组件第一次被渲染时,也会被布局,该方法会记录相应的布局信息,并通过
     * this.setState 更新到 this.state, 这次 this.setState 调用会引发一次渲染,
     * 这次渲染主要是渲染子组件,而不会引起再次调用该布局回调函数
     *
     * @param event
     * @private
     */
    _onLayout(event) {
        const {height, width} = event.nativeEvent.layout;

        if (
            this.state.layout.initWidth === width &&
            this.state.layout.initHeight === height
        ) {
            // 这段代码保证仅在第一次布局的时候获取相应的信息,
            // 避免不必要的计算
            return;
        }

        const layout = {
            ...this.state.layout,
            initHeight: height, // 重新设置布局的 initHeight
            initWidth: width, //  重新设置布局的 initWidth
            isMeasured: true, // 设置 layout 对象为 已经经过计算得出
        };

        layout.height.setValue(height);
        layout.width.setValue(width);

        const nextState = {
            ...this.state,
            layout,
        };

        this._transitionProps = buildTransitionProps(this.props, nextState);
        //  设置新的状态,触发一次渲染动作
        this.setState(nextState);
    }

    _onTransitionEnd() {
        if (!this._isMounted) {
            // 处理动画播放过程中组件被卸载的情况 : 不再继续执行了,组件都没了
            return;
        }
        const prevTransitionProps = this._prevTransitionProps;
        this._prevTransitionProps = null;

        // 筛选出所有没有过期的场景
        const scenes = this.state.scenes.filter(isSceneNotStale);

        const nextState = {
            ...this.state,
            /**
             * Array.prototype.filter creates a new instance of an array
             * even if there were no elements removed. There are cases when
             * `this.state.scenes` will have no stale scenes (typically when
             * pushing a new route). As a result, components that rely on this prop
             * might enter an unnecessary render cycle.
             */
            scenes:
                this.state.scenes.length === scenes.length ? this.state.scenes : scenes,
        };

        this._transitionProps = buildTransitionProps(this.props, nextState);

        // 设置 this.state 为新的状态 nextState, 并在状态设置完成时,
        // 1.调用外部指定的场景切换动画结束回调函数 onTransitionEnd, 如果该回调函数是一个
        //   Promise, 等到该 Promise 执行结果返回
        // 2.如果发现在动画播放过程中有缓存的屏幕切换动画过渡请求(记录在this._queuedTransition里面),
        //   直接启动之,屏幕切换动画过渡状态 _isTransitionRunning 保持 true; 如果没有需要缓存的屏幕
        //   切换动画过渡请求需要处理,设置切换动画过渡状态 _isTransitionRunning 为 false
        this.setState(nextState, async () => {
            if (this.props.onTransitionEnd) {
                const result = this.props.onTransitionEnd(
                    this._transitionProps,
                    prevTransitionProps
                );

                if (result instanceof Promise) {
                    await result;
                }
            }

            if (this._queuedTransition) {
                // 如果有缓存的屏幕过渡动画请求,启动它,
                // 屏幕切换动画过渡状态 _isTransitionRunning 保持 true
                this._startTransition(
                    this._queuedTransition.nextProps,
                    this._queuedTransition.nextScenes,
                    this._queuedTransition.indexHasChanged
                );
                // 清除缓存的屏幕过渡动画请求
                this._queuedTransition = null;
            } else {
                // 如果没有缓存的屏幕过渡动画请求,
                // 屏幕切换动画过渡状态 _isTransitionRunning 设置到 false
                this._isTransitionRunning = false;
            }
        });
    }
}

/**
 * 根据属性 props 和状态 state 计算,构造屏幕切换过渡属性对象
 * @param props 当前 Transitioner 组件的属性
 * @param state 当前 Transitioner 组件的状态
 * 一个例子 :
 * {
 *  "layout":{
 *      "height":568,
 *      "initHeight":568,
 *      "initWidth":384,
 *      "isMeasured":true, // true 表示已经经过布局计算
 *      "width":384
 *  },
 *  "position":1,
 *  "progress":0,
 *  "scenes":[
 *      {
 *          "index":0,
 *          "isActive":true,
 *          "isStale":false,
 *          "key":"scene_id-1527489346447-0",
 *          "route":{
 *              "routes":[
 *                  {"key":"HomeScreen","routeName":"HomeScreen"},
 *                  {"key":"MessageScreen","routeName":"MessageScreen"},
 *                  {"key":"OrderScreen","routeName":"OrderScreen"},
 *                  {"key":"MineScreen","routeName":"MineScreen"}
 *                  ],
 *              "index":0,
 *              "isTransitioning":false,
 *              "routeName":"TabScreens",
 *              "key":"id-1527489346447-0"
 *          }
 *      },
 *      {
 *          "index":1,
 *          "isActive":false,
 *          "isStale":true,
 *          "key":"scene_id-1527489346447-1",
 *          "route":{"params":{},"routeName":"SettingScreen","key":"id-1527489346447-1"}
 *      }
 *  ]
 * }
 * @return {{layout: *, navigation: *, position: *, progress: *, scenes: *, scene: *, index: *}}
 */
function buildTransitionProps(props, state) {
    const {navigation} = props;

    const {layout, position, progress, scenes} = state;

    const scene = scenes.find(isSceneActive);

    invariant(scene, 'Could not find active scene');

    return {
        // 例子 :
        // "layout":{"height":568,"initHeight":568,"initWidth":384,"isMeasured":true,"width":384}
        layout, // 当前  Transitioner 组件根 View 的布局信息
        navigation, // 当前组件的导航信息
        position, // 屏幕切换过渡动画起始场景的索引,Animated.Value
        progress, // 屏幕切换过渡动画执行进度:0->1,Animated.Value
        scenes, // 所有导航栈中的场景屏幕(Array)
        scene, // 当前活跃场景屏幕
        index: scene.index, // 当前活跃场景屏幕的索引
    };
}

/**
 * 如果某个场景的属性 isStale 为 true, 表明它是一个过期不用的场景
 * @param scene
 * @return {boolean}
 */
function isSceneNotStale(scene) {
    return !scene.isStale;
}

/**
 * 如果某个场景的属性 isActive 为 true,表明它是当前活跃场景,
 * 也就是导航栈栈顶的那个路由场景屏幕
 * @param scene
 * @return {boolean}
 */
function isSceneActive(scene) {
    return scene.isActive;
}

const styles = StyleSheet.create({
    main: {
        flex: 1,
    },
});

export default Transitioner;

猜你喜欢

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