編輯:增加了一個工作示例
https://lab.award.is/react-shared-element-transition-example/
(有些在Safari的問題適用於MacOS我)
的想法是必須的動畫元素包裹在一個容器中,可以在安裝時存儲其位置。我創建了一個名爲SharedElement
的簡單React組件,它完全可以做到這一點。
所以一步爲你的例子(Overview
視圖和Detailview
)步驟:
- 的
Overview
視圖獲取安裝。概述中的每個項目(方塊)包含在帶有唯一ID的SharedElement
中(例如項目-,項目-1等)。 SharedElement
組件將每個項目的位置存儲在靜態Store
變量中(通過您給予的ID)。
- 您導航到
Detailview
。詳細視圖被包裹到另一個SharedElement
中,該ID與您單擊的項目具有相同的ID,例如item-4。
- 現在,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'},
})
}
}
您可以使用帶有生命週期掛鉤的使用GreenSock動畫庫,使一些真正很酷的過渡。 –
你有沒有取得任何進展?我試圖實現這樣的東西,並提出了一個非常基本的解決方案,但它的工作原理。我受到了Android的共享元素轉換和exponentjs/ex-navigation(React Native)中的實現的啓發。如果您在發佈我的資料之前找到了一個好的可重複使用的解決方案,那將是一件好事。 – tommy
FYI react-router v4(仍然在alpha版,也許beta版)已經取消了自定義生命週期掛鉤,並且簡單地使用標準組件生命週期掛鉤,這可能會讓你做你想做的事 –