使用 React Router v4 和 react-transition-group v2 的页面滑动动画

page sliding animation with React Router v4 and react-transition-group v2

我正在使用 React Router v4 和 react-transition-group v2 来测试页面滑动动画。

const RouterMap = () => (
  <Router>
    <Route render={({ location }) =>
      <TransitionGroup>
        <CSSTransition key={location.pathname.split('/')[1]} timeout={500} classNames="pageSlider" mountOnEnter={true} unmountOnExit={true}>
          <Switch location={location}>
            <Route path="/" exact component={ Index } />
            <Route path="/comments" component={ Comments } />
            <Route path="/opinions" component={ Opinions } />
            <Route path="/games" component={ Games } />
          </Switch>
        </CSSTransition>
      </TransitionGroup>
    } />
  </Router>
)

和 CSS:

.pageSlider-enter {
  transform: translate3d(100%, 0, 0);
}

.pageSlider-enter.pageSlider-enter-active {
  transform: translate3d(0, 0, 0);
  transition: all 600ms;
}
.pageSlider-exit {
  transform: translate3d(0, 0, 0);
}

.pageSlider-exit.pageSlider-exit-active {
  transform: translate3d(-100%, 0, 0);
  transition: all 600ms;
}

动画如下:

如你所见,index page 滑动到 detail page 的动画是完全正确的(从右到左)。但是当我点击Back图标时,我希望index page从左到右出现。

我知道如果我将 CSS 更改为如下所示,页面将从左到右出现:

.pageSlider-enter {
  transform: translate3d(-100%, 0, 0);
}
.pageSlider-exit.pageSlider-exit-active {
  transform: translate3d(100%, 0, 0);
  transition: all 600ms;
}

但是如何将两个动画组合在一起呢?一般来说,每当用户点击后退图标时,动画应该是 from left to right.


更新时间:2017.08.31

感谢@MatijaG,使用路径深度真是一个很棒的主意。我跟着它,遇到了一个新问题。

function getPathDepth(location) {
  let pathArr = (location || {}).pathname.split('/');
  pathArr = pathArr.filter(n => n !== '');
  return pathArr.length;
}

<Route render={({ location }) =>
  <TransitionGroup>
    <CSSTransition
      key={location.pathname.split('/')[1]}
      timeout={500}
      classNames={ getPathDepth(location) - this.state.prevDepth > 0 ? 'pageSliderLeft' : 'pageSliderRight' }
      mountOnEnter={true}
      unmountOnExit={true}
    >
      <Switch location={location}>
        <Route path="/" exact component={ Index } />
        <Route path="/comments" component={ Comments } />
        <Route path="/opinions" component={ Opinions } />
        <Route path="/games/lol" component={ LOL } /> // add a new route
        <Route path="/games" component={ Games } />
      </Switch>
    </CSSTransition>
  </TransitionGroup>
} />

并更新了CSS:

.pageSliderLeft-enter {
  transform: translate3d(100%, 0, 0);
}
.pageSliderLeft-enter.pageSliderLeft-enter-active {
  transform: translate3d(0, 0, 0);
  transition: all 600ms;
}
.pageSliderLeft-exit {
  transform: translate3d(0, 0, 0);
}
.pageSliderLeft-exit.pageSliderLeft-exit-active {
  transform: translate3d(100%, 0, 0);
  transition: all 600ms;
}

.pageSliderRight-enter {
  transform: translate3d(-100%, 0, 0);
}
.pageSliderRight-enter.pageSliderRight-enter-active {
  transform: translate3d(0, 0, 0);
  transition: all 600ms;
}
.pageSliderRight-exit {
  transform: translate3d(0, 0, 0);
}
.pageSliderRight-exit.pageSliderRight-exit-active {
  transform: translate3d(-100%, 0, 0);
  transition: all 600ms;
}

动画:

从'/'到'/games'可以,从'/games'回到'/'也可以(类型1:路线A -> 路线B,只有2条路线)。但是如果先从'/'到'/games',然后从'/games'到'/games/lol',第二阶段失去动画(类型2:route A -> route B -> 路线 C,3 条或更多路线)。我们还看到从'/games/lol'回到'/games'再回到'/',幻灯片动画与类型1不一样。

有人知道这个问题吗?

主要问题是,即使在您的问题中,您也没有指定应用程序应如何知道使用哪个方向。

一种方法是使用路径 depth/length:如果您导航到的路径比当前路径 "deeper",则从右到左过渡,但如果过渡较浅从左到右?

另一个问题是路由器只给你你要去的位置,所以你应该保存以前的位置(或深度),这样你就可以比较它。

之后就是在 css 个类名之间切换, 沿着这条线:

import React from 'common/react'
import { withRouter, Route, Switch } from 'react-router'

function getPathDepth (location) {
    return (location || {} ).pathname.split('/').length
}
class RouterMap extends React.PureComponent {
    constructor (props, context) {
        super(props, context)
        this.state = {
            prevDepth: getPathDepth(props.location),
        }
    }

    componentWillReceiveProps () {
        this.setState({ prevDepth: getPathDepth(this.props.location) })
    }
    render () {

        return (
            <Router>
                <Route render={ ({ location }) =>
                    (<TransitionGroup>
                        <CSSTransition
                            key={ location.pathname.split('/')[1] }
                            timeout={ 500 }
                            classNames={ getPathDepth(location) - this.state.prevDepth ? 'pageSliderLeft' : 'pageSliderRight' } mountOnEnter={ true } unmountOnExit={ true }
                        >
                            <Switch location={ location }>
                                <Route path='/' exact component={ Index } />
                                <Route path='/comments' component={ Comments } />
                                <Route path='/opinions' component={ Opinions } />
                                <Route path='/games' component={ Games } />
                            </Switch>
                        </CSSTransition>
                    </TransitionGroup>)
                }
                />
            </Router>
        )
    }
}

export default withRouter(RouterMap)

Codepen 示例:https://codepen.io/matijag/pen/ayXpGr?editors=0110

扩展答案以容纳更多页面并保持左右过渡顺序。

import React from 'react';
import './assets/scss/styles.scss';
import { Route, Switch, withRouter  } from "react-router-dom";
import {TransitionGroup, CSSTransition } from 'react-transition-group';
import Header from './components/Header';
import Home from './pages/Home';
import Yellow from './pages/Yellow';
import Red from './pages/Red';
import Green from './pages/Green';
import Blue from './pages/Blue';
import Purple from './pages/Purple';
const pages = [
    { path: '/', name: 'home', order: 1 },
    { path: '/yellow', name: 'yellow', order: 2 },
    { path: '/red', name: 'red', order: 3 },
    { path: '/green', name: 'green', order: 4 },
    { path: '/blue', name: 'blue', order: 5 },
    { path: '/purple', name: 'purple', order: 6 }
]

class App extends React.Component {
    constructor(props){
        super(props)
        this.state = {
            currentPage: this.setPage(this.props.location.pathname),
            curPageOrder: this.setCurrentOrder(this.props.location.pathname),
            newPageOrder: null
        }
    }
    componentDidUpdate(prevProps, prevState){
        console.log('Component did update');

        let newPage = this.setPage(this.props.location.pathname);
        let newPageOrder = pages.filter(function (page) {
            return page.name === newPage;
        });

        let curPage = this.state.currentPage;
        let curPageOrder = pages.filter(function (page) {
            return page.name === curPage;
        });

        if( newPage !== curPage){
            console.log('new page');
            let direction = curPageOrder[0].order < newPageOrder[0].order ? 'left' : 'right';
            // Set State
            this.setState({
                currentPage: newPage,
                pageDirection: direction,
                curPageOrder: curPageOrder[0].order,
                newPageOrder: newPageOrder[0].order,
            })

        }

    }
    setCurrentOrder = (path) => {
        let curPageOrder = pages.filter(function (page) {
            return page.path === path;
        });

        return curPageOrder[0].order;
    }

    setPage = (pathname) => {
        // SET PAGE FOR CSS CLASSES
        let page = null;
        switch (pathname){
            case('/'):
                page = 'home';
                break;
            case('/yellow'):
                page = 'yellow';
                break;
            case('/red'):
                page = 'red';
                break;
            case('/green'):
                page = 'green';
                break;
            case('/blue'):
                page = 'blue'
                break;
            case('/purple'):
                page = 'purple';
                break;
            default:
                page = 'home';

        }

        return page;
    }
    render() {
        const { location } = this.props;
        const currentKey = location.pathname.split("/")[1] || "/";


        return (
            <div className={`wrapper ${this.setPage(this.props.location.pathname)}`}>
                <Header />
                <div className={`wrap ${currentKey} `}>
                    <TransitionGroup  className={`transition-group ${this.state.pageDirection}`}>
                        <CSSTransition
                            key={currentKey}
                            timeout={{ enter: 800, exit: 400 }}
                            classNames={'transition-wrap'}

                        >

                            <section className={`route-section fade`}>
                                <Switch location={location}>
                                    <Route exact path="/" component={() => <Home />} />
                                    <Route path="/yellow" component={() => <Yellow /> } />
                                    <Route path="/red" component={() => <Red /> } />
                                    <Route path="/green" component={() => <Green /> } />
                                    <Route path="/blue" component={() => <Blue /> } />
                                    <Route path="/purple" component={() => <Purple /> } />   
                                </Switch>
                            </section>

                        </CSSTransition>
                    </TransitionGroup>
                </div>
            </div>
        )
    }
}
export default withRouter(App);

https://codesandbox.io/s/github/darbley/react-page-slide-transitions

我带来了@Darbley 答案的简化 "hooked" 版本,它工作可靠

App.js

import React, { useEffect, useState } from 'react'
import styled from 'styled-components/macro'
import { hot } from 'react-hot-loader/root'
import { setConfig } from 'react-hot-loader'
import { Switch, Route } from 'react-router-dom'
import { TransitionGroup, CSSTransition } from 'react-transition-group'

import { compose } from 'redux'
import { withRouter } from '@hoc'

import Home from '@components/Home/Home'
import About from './About/About'
import Contact from './Contact/Contact'

import Navbar from './Navbar/Navbar'

setConfig({
    reloadHooks: false
})

const AppContainer = styled.main``

const pages = [
    { path: '/', order: 1 },
    { path: '/about', order: 2 },
    { path: '/contact', order: 3 }
]

const App = ({ location }) => {
    const [pageDirection, setPageDirection] = useState()
    const [currentPath, setCurrentPath] = useState(location.pathname)
    const [currentPathOrder, setCurrentPathOrder] = useState(
        pages.filter(({ path }) => path === location.pathname)[0].order
    )
    const currentKey = location.pathname.split('/')[1] || '/'
    useEffect(() => {
        const newPath = location.pathname
        const newPathOrder = pages.filter(({ path }) => path === newPath)[0].order
        if (newPath !== currentPath) {
            const direction = currentPathOrder < newPathOrder ? 'left' : 'right'
            setCurrentPath(newPath)
            setCurrentPathOrder(newPathOrder)
            setPageDirection(direction)
        }
    })
    return (
        <AppContainer>
            <Navbar />
            <TransitionGroup className={`${pageDirection}`}>
                <CSSTransition key={currentKey} timeout={1000} classNames={'route'}>
                    <div className="route__container">
                        <Switch location={location}>
                            <Route path="/" exact component={Home} />
                            <Route path="/about" component={About} />
                            <Route path="/contact" component={Contact} />
                        </Switch>
                    </div>
                </CSSTransition>
            </TransitionGroup>
        </AppContainer>
    )
}

export default process.env.NODE_ENV === 'development'
    ? compose(withRouter)(hot(App))
    : compose(withRouter)(App)

index.scss

.route__container {
    width: 100%;
    height: 100vh;
    position: absolute;
    top: 0px;
    left: 0px;
    transition: 1s;
}

.left .route-enter {
    transform: translateX(100%);
}

.left .route-enter-active {
    transform: translateX(0%);
}

.left .route-exit {
    transform: translateX(-100%);
}

.left .route-exit-active {
    transform: translateX(-100%);
}

.right .route-enter {
    transform: translateX(-100%);
}

.right .route-enter-active {
    transform: translateX(0%);
}

.right .route-exit {
    transform: translateX(100%);
}

.right .route-exit-active {
    transform: translateX(100%);
}

我写了一个 AnimatedSwitch for react router v4 native。

https://javascriptrambling.blogspot.com/2020/07/react-router-native-animatedswitch.html

我使用 history.action 来确定我是向前还是向后(即滑入或滑出)。然后确保我的过渡使用适当的动作来匹配我想要的动画。使用 history.goBack 或 history.replace 将其滑出。正常history.push还是会滑进去

      if (history.action === 'POP' || history.action === 'REPLACE') {
        setNewAnimationStyle(animationType.backNew(anim));
        setPrevAnimationStyle(animationType.backPrevious(anim));
      } else {
        setNewAnimationStyle(animationType.new(anim));
        setPrevAnimationStyle(animationType.previous(anim));
      }