Анимированные переходы страниц в react
последние пару недель я работал над приложением, используя React. Пока все работает нормально, но теперь я хочу добавить к нему некоторые переходы. Эти переходы немного сложнее, чем любые примеры, которые мне удалось найти.
у меня есть 2 страницы, Обзор и подробная страница, которую я хотел бы перейти между ними.
Я использую react-router для управления моими маршрутами:
<Route path='/' component={CoreLayout}>
<Route path=':pageSlug' component={Overview} />
<Route path=':pageSlug/:detailSlug' component={DetailView} />
</Route>
идея перехода заключается в том, что вы нажимаете на один из элементов обзора. Этот элемент, который был нажат, перемещается к позиции, которую он должен иметь в detailView. Переход должен быть инициирован изменением маршрута (я думаю), а также должен иметь возможность происходить в обратном порядке.
Я уже пробовал использовать ReactTransitionGroup
на макете, который имеет метод рендеринга, который выглядит так:
render () {
return (
<div className='layout'>
<ReactTransitionGroup>
React.cloneElement(this.props.children, { key: this.props.location.pathname })
</ReactTransitionGroup>
</div>
)
}
Это даст дочернему компоненту возможность получить специальный жизненный цикл крючки. Но я хотел бы получить доступ к дочерним компонентам как-то во время этих крючков и все еще продолжать делать все так, как реагируют.
кто-то может мне точку в правильном направлении для следующего шага? Или, может быть, указать мне на пример, который я, возможно, пропустил где-то? В предыдущих проектах я использовал Эмбер вместе с жидкий огонь чтобы получить такие переходы, может быть, есть что-то вроде этого для React?
Я использую реагировать/react-redux/react-маршрутизатор/react-router-redux.
2 ответов
Edit: добавлен рабочий пример
https://lab.award.is/react-shared-element-transition-example/
(некоторые проблемы в Safari для macOS для меня)
идея состоит в том, чтобы анимировать элементы, завернутые в контейнер, который сохраняет свои позиции при монтаже. Я создал простой компонент React под названием SharedElement
что делает именно это.
так шаг за шагом для пример (Overview
вид Detailview
):
- на
Overview
вид монтируется. Каждый элемент (квадраты) внутри обзора завернуты вSharedElement
с уникальным идентификатором (например,элемент-0, пункт 1 и т. д.). TheSharedElement
компонент сохраняет позицию для каждого элемента в статическомStore
переменная (по идентификатору, который вы им дали). - перейти к
Detailview
. Detailview завернут в другойSharedElement
что имеет тот же идентификатор, что и элемент, на который вы нажали, например пункт 4. - теперь на этот раз SharedElement видит, что элемент с тем же идентификатором уже зарегистрирован в своем магазине. Он будет клонировать новый элемент, применять к нему старую позицию элементов (из Detailview) и анимировать в новую позицию (я сделал это с помощью GSAP). Когда анимация завершена, она перезаписывает новую позицию для элемента в магазине.
используя этот метод, он фактически не зависит от маршрутизатора React (никаких специальных методов жизненного цикла, но componentDidMount
), и он даже будет работать при посадке на обзорной странице сначала и навигации по обзорной странице.
я поделюсь с вами своей реализацией, но имейте в виду, что у нее есть некоторые известные ошибки. Е. Г. вам придется иметь дело с Z-индексами и выходит из себя; и он не обрабатывает удаление позиции элементов из магазина еще нет. Я уверен, что кто-то может потратить на это время, вы можете сделать из него отличный маленький плагин.
реализация:
.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>
)
}
}
обзор.js - обратите внимание на идентификатор SharedElement
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
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'},
})
}
}
у меня на самом деле была аналогичная проблема, когда у меня была панель поиска и я хотел, чтобы она перемещалась и сворачивалась в другой размер и место на определенном маршруте (например, общий поиск в navbar и выделенной странице поиска). По этой причине я создал компонент, очень похожий на SharedElement выше.
компонент ожидает в качестве реквизита, a singularKey
и singularPriority
и чем вы оказываете компонента в сервала места, но компонент будет только сделать самый высокий приоритет, а не животным он.
компонент на npm
as react-singular-compoment
И вот страница GitHub для документов.