响应中的动画页面转换

时间:2016-01-26 18:04:14

标签: animation reactjs transitions

在过去的几周里,我一直在使用React开发应用。到目前为止一切正常,但现在我想添加一些过渡。这些转换比我设法找到的任何示例都要复杂一些。

我有2页,概述和详细信息页面,我希望在它们之间进行转换。

我使用react-router来管理我的路线:

<Route path='/' component={CoreLayout}>

  <Route path=':pageSlug' component={Overview} />
  <Route path=':pageSlug/:detailSlug' component={DetailView} />

</Route>

概述如下所示: enter image description here

Detailview看起来像这样: enter image description here

转换的想法是您单击概述的其中一个元素。单击此元素将移动到detailView上应该具有的位置。过渡应该通过路线改变(我认为)来启动,并且也应该能够反过来发生。

我已经尝试在布局上使用ReactTransitionGroup,它有一个如下所示的渲染方法:

render () {
    return (
        <div className='layout'>
            <ReactTransitionGroup>
                React.cloneElement(this.props.children, { key: this.props.location.pathname })
            </ReactTransitionGroup>
        </div>
    )
}

这将使子组件能够接收特殊lifecycle hooks。但是我想在这些钩子中以某种方式访问​​子组件,并且仍然以React方式继续操作。

有人能指出我正确的方向采取下一步措施吗?或者也许请指出一个我可能错过某个地方的例子?在之前的项目中,我使用Emberliquid fire来获得这些类型的转换,React可能有类似的内容吗?

我正在使用react / react-redux / react-router / react-router-redux

2 个答案:

答案 0 :(得分:4)

编辑:添加了一个工作示例

https://lab.award.is/react-shared-element-transition-example/

(Safari for macOS中的一些问题)

我们的想法是将要动画的元素包装在容器中,该容器在安装时存储其位置。我创建了一个名为SharedElement的简单React组件,它正是这样做的。

一步一步为您的示例(Overview查看和Detailview):

  1. Overview视图已装入。概述中的每个(正方形)都包含在SharedElement中,并带有唯一ID(例如 item-0 item-1 < / em>等)。 SharedElement组件将每个项目的位置存储在静态Store变量中(按您提供的ID)。
  2. 导航至Detailview。 Detailview被包装到另一个SharedElement中,其ID与您点击的项目具有相同的ID,例如 item-4
  3. 现在,这次,SharedElement看到具有相同ID的项目已在其商店中注册。它将克隆新元素,将旧元素位置应用于它(来自Detailview的那个)并动画到新位置(我使用GSAP完成它)。动画完成后,它将覆盖商店中项目的新位置。
  4. 使用这种技术,它实际上独立于React Router(没有特殊的生命周期方法,只有componentDidMount),它甚至可以在首次登陆Overview页面并导航到Overview页面时工作。

    我将与您分享我的实现,但请注意它有一些已知的错误。例如。你必须自己处理z-indeces和溢出;并且它还没有处理来自商店的取消注册元素位置。我很确定如果有人可以花一些时间在这上面,你可以用它制作一个很棒的小插件。

    实施:

    index.js

    import React from "react";
    import ReactDOM from "react-dom";
    import App from "./App";
    
    import Overview from './Overview'
    import DetailView from './DetailView'
    
    import "./index.css";
    
    import { Router, Route, IndexRoute, hashHistory } from 'react-router'
    
    const routes = (
        <Router history={hashHistory}>
            <Route path="/" component={App}>
                <IndexRoute component={Overview} />
                <Route path="detail/:id" component={DetailView} />
            </Route>
        </Router>
    )
    
    ReactDOM.render(
        routes,
        document.getElementById('root')
    );
    

    App.js

    import React, {Component} from "react"
    import "./App.css"
    
    export default class App extends Component {
        render() {
            return (
                <div className="App">
                    {this.props.children}
                </div>
            )
        }
    }
    

    Overview.js - 请注意SharedElement上的ID

    import React, { Component } from 'react'
    import './Overview.css'
    import items from './items' // Simple array containing objects like {title: '...'}
    import { hashHistory } from 'react-router'
    import SharedElement from './SharedElement'
    
    export default class Overview extends Component {
    
        showDetail = (e, id) => {
            e.preventDefault()
    
            hashHistory.push(`/detail/${id}`)
        }
    
        render() {
            return (
                <div className="Overview">
                    {items.map((item, index) => {
                        return (
                            <div className="ItemOuter" key={`outer-${index}`}>
                                <SharedElement id={`item-${index}`}>
                                    <a
                                        className="Item"
                                        key={`overview-item`}
                                        onClick={e => this.showDetail(e, index + 1)}
                                    >
                                        <div className="Item-image">
                                            <img src={require(`./img/${index + 1}.jpg`)} alt=""/>
                                        </div>
    
                                        {item.title}
                                    </a>
                                </SharedElement>
                            </div>
                        )
                    })}
                </div>
            )
        }
    
    }
    

    DetailView.js - 注意SharedElement上的ID

    import React, { Component } from 'react'
    import './DetailItem.css'
    import items from './items'
    import { hashHistory } from 'react-router'
    import SharedElement from './SharedElement'
    
    export default class DetailView extends Component {
    
        getItem = () => {
            return items[this.props.params.id - 1]
        }
    
        showHome = e => {
            e.preventDefault()
    
            hashHistory.push(`/`)
        }
    
        render() {
            const item = this.getItem()
    
            return (
                <div className="DetailItemOuter">
                    <SharedElement id={`item-${this.props.params.id - 1}`}>
                        <div className="DetailItem" onClick={this.showHome}>
                            <div className="DetailItem-image">
                                <img src={require(`./img/${this.props.params.id}.jpg`)} alt=""/>
                            </div>
                            Full title: {item.title}
                        </div>
                    </SharedElement>
                </div>
            )
        }
    
    }
    

    SharedElement.js

    import React, { Component, PropTypes, cloneElement } from 'react'
    import { findDOMNode } from 'react-dom'
    import TweenMax, { Power3 } from 'gsap'
    
    export default class SharedElement extends Component {
    
        static Store = {}
        element = null
    
        static props = {
            id: PropTypes.string.isRequired,
            children: PropTypes.element.isRequired,
            duration: PropTypes.number,
            delay: PropTypes.number,
            keepPosition: PropTypes.bool,
        }
    
        static defaultProps = {
            duration: 0.4,
            delay: 0,
            keepPosition: false,
        }
    
        storeNewPosition(rect) {
            SharedElement.Store[this.props.id] = rect
        }
    
        componentDidMount() {
            // Figure out the position of the new element
            const node = findDOMNode(this.element)
            const rect = node.getBoundingClientRect()
            const newPosition = {
                width: rect.width,
                height: rect.height,
            }
    
            if ( ! this.props.keepPosition) {
                newPosition.top = rect.top
                newPosition.left = rect.left
            }
    
            if (SharedElement.Store.hasOwnProperty(this.props.id)) {
                // Element was already mounted, animate
                const oldPosition = SharedElement.Store[this.props.id]
    
                TweenMax.fromTo(node, this.props.duration, oldPosition, {
                    ...newPosition,
                    ease: Power3.easeInOut,
                    delay: this.props.delay,
                    onComplete: () => this.storeNewPosition(newPosition)
                })
            }
            else {
                setTimeout(() => { // Fix for 'rect' having wrong dimensions
                    this.storeNewPosition(newPosition)
                }, 50)
            }
        }
    
        render() {
            return cloneElement(this.props.children, {
                ...this.props.children.props,
                ref: element => this.element = element,
                style: {...this.props.children.props.style || {}, position: 'absolute'},
            })
        }
    
    }
    

答案 1 :(得分:0)

我实际上有一个类似的问题,我有一个搜索栏,希望它移动并包装成不同的大小,然后放在特定的路线上(例如导航栏中的常规搜索和专用的搜索页面)。因此,我创建了一个与上面的SharedElement非常相似的组件。

组件希望将singularKeysingularPriority作为道具,然后将其渲染到服务位置,但是该组件只会渲染最高优先级并为其设置动画。

该组件在npm上的名称为react-singular-compoment 这是GitHub page for the docs