Анимированные переходы страниц в react

последние пару недель я работал над приложением, используя 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>
    )
}

Это даст дочернему компоненту возможность получить специальный жизненный цикл крючки. Но я хотел бы получить доступ к дочерним компонентам как-то во время этих крючков и все еще продолжать делать все так, как реагируют.

кто-то может мне точку в правильном направлении для следующего шага? Или, может быть, указать мне на пример, который я, возможно, пропустил где-то? В предыдущих проектах я использовал Эмбер вместе с жидкий огонь чтобы получить такие переходы, может быть, есть что-то вроде этого для React?

Я использую реагировать/react-redux/react-маршрутизатор/react-router-redux.

2 ответов


Edit: добавлен рабочий пример

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

(некоторые проблемы в Safari для macOS для меня)


идея состоит в том, чтобы анимировать элементы, завернутые в контейнер, который сохраняет свои позиции при монтаже. Я создал простой компонент React под названием SharedElement что делает именно это.

так шаг за шагом для пример (Overview вид Detailview):

  1. на Overview вид монтируется. Каждый элемент (квадраты) внутри обзора завернуты в SharedElement с уникальным идентификатором (например,элемент-0, пункт 1 и т. д.). The SharedElement компонент сохраняет позицию для каждого элемента в статическом Store переменная (по идентификатору, который вы им дали).
  2. перейти к Detailview. Detailview завернут в другой SharedElement что имеет тот же идентификатор, что и элемент, на который вы нажали, например пункт 4.
  3. теперь на этот раз 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 для документов.