import React from 'react';
import {Animated, Easing, StyleSheet, View} from 'react-native';
import invariant from '../utils/invariant';
import NavigationScenesReducer from './ScenesReducer';
const DefaultTransitionSpec = {
duration: 250,
easing: Easing.inOut(Easing.ease),
timing: Animated.timing,
};
/**
* 屏幕切换过渡器, transitioner,
* 目前仅被 CardStackTransitioner 使用,也就是在 StackNavigator 中使用,
* 用于屏幕切换时的动画过渡控制
*/
class Transitioner extends React.Component {
constructor(props, context) {
super(props, context);
const layout = {
height: new Animated.Value(0),
initHeight: 0,
initWidth: 0,
isMeasured: false,
width: new Animated.Value(0),
};
this.state = {
layout,
position: new Animated.Value(this.props.navigation.state.index),
progress: new Animated.Value(1),
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 = {nextProps, nextScenes, indexHasChanged};
return;
}
this._startTransition(nextProps, nextScenes, indexHasChanged);
}
_startTransition(nextProps, nextScenes, indexHasChanged) {
const nextState = {
...this.state,
scenes: nextScenes,
};
const {position, progress} = nextState;
progress.setValue(0);
this._prevTransitionProps = this._transitionProps;
this._transitionProps = buildTransitionProps(nextProps, nextState);
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;
const animations =
indexHasChanged && positionHasChanged
? [
timing(progress, {
...transitionSpec,
toValue: 1,
}),
timing(position, {
...transitionSpec,
toValue: nextProps.navigation.state.index,
}),
]
: [];
this._isTransitionRunning = true;
this.setState(nextState, async () => {
if (nextProps.onTransitionStart) {
const result = nextProps.onTransitionStart(
this._transitionProps,
this._prevTransitionProps
);
if (result instanceof Promise) {
await result;
}
}
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,
initWidth: width,
isMeasured: true,
};
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.setState(nextState, async () => {
if (this.props.onTransitionEnd) {
const result = this.props.onTransitionEnd(
this._transitionProps,
prevTransitionProps
);
if (result instanceof Promise) {
await result;
}
}
if (this._queuedTransition) {
this._startTransition(
this._queuedTransition.nextProps,
this._queuedTransition.nextScenes,
this._queuedTransition.indexHasChanged
);
this._queuedTransition = null;
} else {
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,
navigation,
position,
progress,
scenes,
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;