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;