2016-01-26 89 views
7

過去幾周我一直在使用React處理應用程序。到目前爲止一切工作正常,但現在我想添加一些轉換。這些轉換比我設法找到的任何示例都要複雜一些。反應中的動畫頁面轉換

我有2頁,一個概述和我想要過渡的詳細頁面。

我使用的反應路由器來管理我的路線:

<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

+0

您可以使用帶有生命週期掛鉤的使用GreenSock動畫庫,使一些真正很酷的過渡。 –

+0

你有沒有取得任何進展?我試圖實現這樣的東西,並提出了一個非常基本的解決方案,但它的工作原理。我受到了Android的共享元素轉換和exponentjs/ex-navigation(React Native)中的實現的啓發。如果您在發佈我的資料之前找到了一個好的可重複使用的解決方案,那將是一件好事。 – tommy

+0

FYI react-router v4(仍然在alpha版,也許beta版)已經取消了自定義生命週期掛鉤,並且簡單地使用標準組件生命週期掛鉤,這可能會讓你做你想做的事 –

回答

3

編輯:增加了一個工作示例

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

(有些在Safari的問題適用於MacOS我)


的想法是必須的動畫元素包裹在一個容器中,可以在安裝時存儲其位置。我創建了一個名爲SharedElement的簡單React組件,它完全可以做到這一點。

所以一步爲你的例子(Overview視圖和Detailview)步驟:

  1. Overview視圖獲取安裝。概述中的每個項目(方塊)包含在帶有唯一ID的SharedElement中(例如項目-,項目-1等)。 SharedElement組件將每個項目的位置存儲在靜態Store變量中(通過您給予的ID)。
  2. 您導航到Detailview。詳細視圖被包裹到另一個SharedElement中,該ID與您單擊的項目具有相同的ID,例如item-4
  3. 現在,SharedElement現在看到具有相同ID的項目已在其商店中註冊。它會克隆新元素,將舊元素的位置(Detailview中的元素)應用到新位置(我使用GSAP執行)。動畫完成後,它將覆蓋商店中商品的新位置。

使用這種技術,它實際上是從陣營路由器(沒有特殊的生命週期方法,但componentDidMount)獨立和降落概述頁面上的第一和導航到概述頁面時,它仍然工作。

我將與您分享我的實施,但請注意它有一些已知的錯誤。例如。你必須處理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 - 注意ID上SharedElement

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'}, 
     }) 
    } 

}