2016-01-26 7 views
7

In den letzten paar Wochen habe ich mit React an einer App gearbeitet. Bis jetzt funktioniert alles gut, aber jetzt möchte ich einige Übergänge hinzufügen. Diese Übergänge sind etwas komplexer als alle Beispiele, die ich gefunden habe.Animierte Seitenübergänge in reagieren

Ich habe 2 Seiten, eine Übersicht und eine Detailseite, zwischen denen ich wechseln möchte.

Ich verwende reagieren-Router meine Routen zu verwalten:

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

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

</Route> 

Übersicht wie folgt aussieht: enter image description here

Detail wie folgt aussieht: enter image description here

Die Idee des Übergangs Sie klicken auf eines der Elemente der Übersicht. Das angeklickte Element bewegt sich in die Position, die es in der Detailansicht haben soll. Der Übergang sollte durch eine Routenänderung initiiert werden (denke ich) und sollte auch umgekehrt möglich sein. Dies gibt die Möglichkeit, das Kind Komponente, um die besondere lifecycle hooks zu erhalten

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

:

ich bereits habe versucht ReactTransitionGroup im Layout verwendet wird, die eine Render-Methode hat, die wie folgt aussieht. Aber ich würde gerne während dieser Hooks irgendwie auf die Child-Komponenten zugreifen und weiterhin Dinge wie React machen.

Kann mir jemand in die richtige Richtung für den nächsten Schritt zeigen? Oder vielleicht zeigen Sie mir ein Beispiel, das ich irgendwo verpasst haben könnte? In früheren Projekten habe ich Ember zusammen mit liquid fire verwendet, um diese Art von Übergängen zu bekommen, gibt es vielleicht so etwas wie React?

Ich verwende react/react-redux/react-router/react-router-redux.

+0

können Sie die GreenSock Animation Bibliothek mit den Lifecycle-Haken verwenden, um einige wirklich zu machen coole Übergänge. –

+0

Haben Sie Fortschritte dabei gemacht? Ich habe versucht, so etwas zu implementieren und eine sehr einfache Lösung gefunden, aber es funktioniert. Ich wurde von Androids Shared Element Transitions und der Implementierung in Exponentialjs/Ex-Navigation (React Native) inspiriert. Es wäre gut zu wissen, ob Sie eine gute und wiederverwendbare Lösung gefunden haben, bevor ich meine Sachen poste. – tommy

+0

FYI react-router v4 (immer noch in Alpha, vielleicht Beta) hat die Custom-Lifecycle-Hooks abgeschafft und benutzt einfach die Standard-Komponenten-Lebenszyklus-Hooks, mit denen Sie das tun können, was Sie zu tun versuchen –

Antwort

3

Edit: Hinzugefügt ein funktionierendes Beispiel

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

(Einige Probleme in Safari für Mac OS für mich)


Die Idee, die Elemente zu haben, ist animiert werden eingewickelt in einen Behälter, der seine Positionen bei der Montage speichert. Ich habe eine einfache React Component namens SharedElement erstellt, die genau das tut.

Schritt So Schritt für Ihr Beispiel (Overview Ansicht und Detailview):

  1. Die Overview Ansicht montiert wird. Jeder Artikel (die Quadrate) innerhalb der Übersicht ist in die SharedElement mit einer eindeutigen ID verpackt (zum Beispiel Artikel-0, Artikel-1 usw.). Die Komponente SharedElement speichert die Position für jedes Element in einer statischen Variablen Store (anhand der von Ihnen angegebenen ID).
  2. Sie navigieren zum Detailview. Die Detailansicht wird in eine andere SharedElement verpackt, die die gleiche ID wie der Artikel hat, auf den Sie geklickt haben, also zum Beispiel item-4.
  3. Jetzt sieht das SharedElement, dass ein Element mit derselben ID bereits in seinem Speicher registriert ist. Es klont das neue Element, wendet die alte Elementposition an (die aus der Detailansicht) und animiert an die neue Position (ich habe es mit GSAP gemacht). Wenn die Animation abgeschlossen ist, überschreibt sie die neue Position für den Artikel im Geschäft.

Mit dieser Technik ist es tatsächlich unabhängig vom Router reagieren (keine besonderen Lebenszyklus Methoden aber componentDidMount), und es wird auch funktionieren, wenn sie auf der Übersichtsseite ersten und die Navigation zu der Übersichtsseite zu landen.

Ich werde meine Implementierung mit Ihnen teilen, aber beachten Sie, dass es einige bekannte Bugs hat. Z.B. Du musst mit Z-Indices umgehen und dich selbst überschwemmen; und es behandelt nicht Registrierung Elementpositionen aus dem Speicher noch nicht. Ich bin mir ziemlich sicher, wenn jemand etwas Zeit damit verbringen kann, kannst du daraus ein tolles kleines Plugin machen.

Die Umsetzung:

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 - Beachten Sie die ID auf der 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 - Beachten Sie die ID auf der 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'}, 
     }) 
    } 

}