react-native 实现转场动画,非react-native-router-flux

前言

因为目前用车是三方(android、ios、web)适配, 用的路由是react-router-native

react-native-router-flux路由自带转场动画效果, 转web报错, 想要做到兼容, 更改成本大

考虑成本, 只能自己实现一个转场动画

转场动画原理

动画开始前, 当前显示页面是b

页面index层级渲染结果
01页面b
10空白

如果是

  1. 确定下一个渲染页面的index, latestStackIndex=1, 当前页面index, oldStackIndex=0

  2. 判断下一个页面, 是新页面还是旧页面, 确定动画的初始值和终值

    1. 如果是新页面, 动画开始值从右往左, startLeft = screenWidthendLeft = 0
    2. 如果是回退到历史页面, 动画开始值从左往右, startLeft = 0endLeft = screenWidth
    3. 如果存在页面不需要动画, 直接设置新页面到动画的终态, startLeft = 0endLeft = 0, 销毁旧页面b
  3. 确定动画页面, 同时动画页面在另一个页面之上, 如果是新的路由页面animatedIndex = latestStackIndex, 如果是回退到历史页面, animatedIndex = oldStackIndex;

  4. 设置动画开始的初始值, 开始动画, 一直更改动画页面的位置

  5. 动画过程中, 就页面b不应该触发渲染

  6. 在动画结束后, 将旧页面b, 销毁

动画结果后

页面index层级渲染结果
00新渲染的页面
11空白

具体实现

1) 路由作为转场动画组建的children

<TransitionAnimation location={location}>
    <RenderRoutes routes={routes} />
</TransitionAnimation>

2) RenderRoutes 路由

    1. Route, Switch 用react-router-dom中的, 而不是react-router-native
    1. 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

    1. Animated.timing动画中指定参数useNativeDriver: false, 因为android会报黄色警告
    1. 样式属性elevation, zIndex 都是控制层级的
    1. 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 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'
    }
});