react-native 实现转场动画(非react-native-router-flux)
前言
因为目前用车是三方(android、ios、web)适配, 用的路由是react-router-native
react-native-router-flux
路由自带转场动画效果, 转web报错, 想要做到兼容, 更改成本大
考虑成本, 只能自己实现一个转场动画
转场动画原理
动画开始前, 当前显示页面是b
页面index | 层级 | 渲染结果 |
---|---|---|
0 | 1 | 页面b |
1 | 0 | 空白 |
如果是
-
确定下一个渲染页面的index,
latestStackIndex=1
, 当前页面index,oldStackIndex=0
-
判断下一个页面, 是新页面还是旧页面, 确定动画的初始值和终值
- 如果是新页面, 动画开始值从右往左,
startLeft = screenWidth
到endLeft = 0
- 如果是回退到历史页面, 动画开始值从左往右,
startLeft = 0
到endLeft = screenWidth
- 如果存在页面不需要动画, 直接设置新页面到动画的终态,
startLeft = 0
到endLeft = 0
, 销毁旧页面b
- 如果是新页面, 动画开始值从右往左,
-
确定动画页面, 同时动画页面在另一个页面之上, 如果是新的路由页面
animatedIndex = latestStackIndex
, 如果是回退到历史页面,animatedIndex = oldStackIndex
; -
设置动画开始的初始值, 开始动画, 一直更改动画页面的位置
-
动画过程中, 就页面b
不应该触发渲染
-
在动画结束后, 将旧页面b,
销毁
动画结果后
页面index | 层级 | 渲染结果 |
---|---|---|
0 | 0 | 新渲染的页面 |
1 | 1 | 空白 |
具体实现
1) 路由作为转场动画组建的children
<TransitionAnimation location={location}>
<RenderRoutes routes={routes} />
</TransitionAnimation>
2) RenderRoutes 路由
-
- Route, Switch 用
react-router-dom
中的, 而不是react-router-native
- Route, Switch 用
-
- Switch中参数location可以将路由受控, 而不是全局中的location
import * as React from 'react';
import { Route, Switch } from 'react-router-dom';
import sendEnterRouter from 'Common/buriedPoint';
export default function renderRoutes(props: { routes: []; extraProps: {}; switchProps: {} }) {
const { routes = [], extraProps = {}, switchProps = {} } = props;
return routes ? (
<Switch {...switchProps}>
{routes.map(
(
route: {
key?: string;
path: string;
exact: boolean;
strict?: boolean;
render?: Function;
component?: Function;
},
i: number
) => {
return route ? (
<Route
key={route.key || i}
path={route.path}
exact={route.exact}
strict={route.strict}
render={(props: object) => {
sendEnterRouter.enterRouter(route.path);
return route.render ? (
route.render({ ...props, ...extraProps, route })
) : (
<route.component {...props} {...extraProps} route={route} />
);
}}
/>
) : null;
}
)}
</Switch>
) : null;
}
3) 转场动画组建TransitionAnimation
-
- Animated.timing动画中指定参数
useNativeDriver: false
, 因为android会报黄色警告
- Animated.timing动画中指定参数
-
- 样式属性
elevation
,zIndex
都是控制层级的
- 样式属性
-
shouldComponentUpdate
方法, 控制是否重新渲染组建
import React from 'react';
import { Animated, View, StyleSheet } from 'react-native';
import utils from 'Common/utils';
interface IProps {
location: {
pathname: string;
};
}
interface IState {
animatedIndex: number;
pathList: Array<string>;
sceneList: Array<ISceneItem>;
}
interface ISceneItem {
pathname: string;
latest: boolean;
destory: boolean;
}
const width = utils.deviceWidthDp;
const defaultSceneItem = {
pathname: '',
latest: false,
destory: false
};
export default class TransitionAnimation extends React.Component<IProps, IState> {
state = {
animatedIndex: 1,
animatedLeft: new Animated.Value(width),
pathList: [],
sceneList: [1, 2].map(() => ({ ...defaultSceneItem }))
};
shouldComponentUpdate(nextProps: IProps, nextState: IState) {
const pathname = this.props.location.pathname;
const nextPathname = nextProps.location.pathname;
const isUpdate = pathname != nextPathname || this.state !== nextState;
if (pathname != nextPathname) {
let { animatedLeft, pathList, sceneList } = this.state;
let oldStackIndex = sceneList.findIndex((item) => item.pathname === pathname); // 旧页面的堆栈下标
const latestStackIndex = oldStackIndex === 1 ? 0 : 1; // 新页面的堆栈下标
oldStackIndex = latestStackIndex === 1 ? 0 : 1; // 保证旧页面的堆栈下标不为-1
const existPageIndex = pathList.findIndex((item) => item === nextPathname); // 渲染过的下标
const isExistPage = existPageIndex > -1; // 是否渲染过
let startLeft: number, // 动画起始
endLeft: number, // 动画终值
animatedIndex: number; // 动画的堆栈下标
if (isExistPage) {
startLeft = 0;
endLeft = width;
animatedIndex = oldStackIndex;
pathList.splice(existPageIndex + 1);
} else {
startLeft = width;
endLeft = 0;
animatedIndex = latestStackIndex;
// 如果是第一个新增页面, 不需要动画
if (pathList.length === 0) {
startLeft = 0;
}
pathList.push(nextPathname);
}
// 不需要动画的页面
if (
['/chooseCar', '/waitCar'].includes(nextPathname) ||
(['/callCar', '/chooseCar', '/waitCar'].includes(pathname) &&
['/callCar', '/chooseCar', '/waitCar'].includes(nextPathname))
) {
sceneList[oldStackIndex] = { pathname: nextPathname, latest: true, destory: false };
sceneList[latestStackIndex] = { pathname: '', latest: false, destory: true };
this.setState({
pathList: [...pathList],
sceneList: [...sceneList]
});
animatedLeft.setValue(0);
return isUpdate;
}
// 更新堆栈
sceneList.forEach((item) => {
item.latest = false;
item.destory = false;
});
sceneList[latestStackIndex] = {
...sceneList[latestStackIndex],
latest: true,
pathname: nextPathname
};
// 开始动画
const newState = {
animatedIndex,
pathList: [...pathList],
sceneList: [...sceneList]
};
animatedLeft.setValue(startLeft);
this.setState(newState, () => {
const { animatedLeft, sceneList } = this.state;
Animated.timing(animatedLeft, {
useNativeDriver: false,
toValue: endLeft,
duration: 300 // 让动画持续一段时间
} as Animated.TimingAnimationConfig).start(({ finished }) => {
if (finished) {
sceneList[oldStackIndex].destory = true;
this.setState({
sceneList: [...sceneList]
});
}
});
});
}
return isUpdate;
}
render() {
const { location, children } = this.props;
const { animatedIndex, animatedLeft, sceneList } = this.state;
return (
<View style={[styles.view]} pointerEvents="box-none">
{sceneList.map((item, index) => {
const isAnimatedIndex = animatedIndex === index;
return (
<Animated.View
key={index}
style={[
styles.animatedView,
{
left: isAnimatedIndex ? animatedLeft : 0,
elevation: isAnimatedIndex ? 1 : 0,
zIndex: isAnimatedIndex ? 1 : 0
}
]}
pointerEvents="box-none">
{!item.destory && (
<Scene index={index} location={location} {...item}>
{children}
</Scene>
)}
</Animated.View>
);
})}
</View>
);
}
}
interface ISceneProps extends ISceneItem {
location: object;
index: number;
}
class Scene extends React.Component<ISceneProps> {
shouldComponentUpdate(nextProps: ISceneProps) {
const { latest, pathname } = this.props;
const { latest: nextLatest, pathname: nextPathname } = nextProps;
const changeLatest = !latest && nextLatest;
const changePathname = latest && nextLatest && pathname !== nextPathname;
return !!(changeLatest || changePathname);
}
render() {
const { children, location } = this.props;
return React.cloneElement(children, {
switchProps: {
location
}
});
}
}
const styles = StyleSheet.create({
view: {
width: '100%',
height: '100%',
overflow: 'hidden'
},
animatedView: {
width: '100%',
height: '100%',
top: 0,
left: 0,
position: 'absolute'
}
});
本博客所记录的文章,主要是从网络收集的,有一些因为经过多次转载,所以出处已经不知,若是侵权,请通知我,我及时修改。本博客主要是用来记录我对所写文章的理解,若有错误,请大家指点,相互学习!