Animation von Layouts mit der FLIP Technik

Avatar of David Khourshid
David Khourshid am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Benutzeroberflächen sind am effektivsten, wenn sie intuitiv und für den Benutzer leicht verständlich sind. Animation spielt dabei eine große Rolle – wie Nick Babich sagte, erweckt Animation Benutzeroberflächen zum Leben. Das Hinzufügen bedeutungsvoller Übergänge und Mikrointeraktionen geschieht jedoch oft erst im Nachhinein oder ist etwas, das „nett zu haben“ ist, wenn die Zeit es erlaubt. Allzu oft erleben wir Webanwendungen, die einfach von einer Ansicht zur nächsten „springen“, ohne dem Benutzer Zeit zu geben, zu verarbeiten, was gerade im aktuellen Kontext geschehen ist.

Dies führt zu unintuitiven Benutzererlebnissen, aber wir können es besser machen, indem wir „Schnittwechsel“ und „Teleportation“ bei der Erstellung von UIs vermeiden. Was ist schließlich natürlicher als das echte Leben, wo nichts teleportiert (außer vielleicht Autoschlüssel), und alles, womit Sie interagieren, sich mit natürlicher Bewegung bewegt?

In diesem Artikel werden wir eine Technik namens „FLIP“ untersuchen, mit der Positionen und Abmessungen beliebiger DOM-Elemente performant animiert werden können, unabhängig davon, wie ihr Layout berechnet oder gerendert wird (z. B. Höhe, Breite, Floats, absolute Positionierung, Transform, Flexbox, Grid usw.)

Warum die FLIP-Technik?

Haben Sie jemals versucht, height, width, top, left oder andere Eigenschaften außer transform und opacity zu animieren? Ihnen ist vielleicht aufgefallen, dass die Animationen etwas ruckelig aussehen, und dafür gibt es einen Grund. Wenn eine Eigenschaft Layoutänderungen auslöst (wie z. B. height), muss der Browser rekursiv prüfen, ob sich dadurch das Layout anderer Elemente geändert hat, und das kann teuer sein. Wenn diese Berechnung länger als ein Animationsframe (etwa 16,7 Millisekunden) dauert, wird der Animationsframe übersprungen, was zu „Ruckeln“ führt, da dieser Frame nicht rechtzeitig gerendert wurde. In Paul Lewis' Artikel „Pixels are Expensive“ geht er detaillierter darauf ein, wie Pixel gerendert werden und welche verschiedenen Leistungskosten damit verbunden sind.

Kurz gesagt, unser Ziel ist es, kurz zu sein – wir wollen die geringstmögliche Anzahl von Stiländerungen so schnell wie möglich berechnen. Der Schlüssel dazu ist, nur transform und opacity zu animieren, und FLIP erklärt, wie wir Layoutänderungen nur mit transform simulieren können.

Was ist FLIP?

FLIP ist ein Mnemotechnikum und eine Technik, die zuerst von Paul Lewis geprägt wurde und für First, Last, Invert, Play steht. Sein Artikel enthält eine ausgezeichnete Erklärung der Technik, aber ich werde sie hier skizzieren

  • First (Erstes): Bevor etwas passiert, erfassen Sie die aktuelle (d. h. erste) Position und Abmessungen des Elements, das übergehen wird. Dazu können Sie element.getBoundingClientRect() verwenden, wie unten gezeigt.
  • Last (Letztes): Führen Sie den Code aus, der den Übergang sofort erfolgen lässt, und erfassen Sie die endgültige (d. h. letzte) Position und Abmessungen des Elements.*
  • Invert (Umkehren): Da sich das Element an der letzten Position befindet, wollen wir die Illusion erzeugen, dass es sich an der ersten Position befindet, indem wir transform verwenden, um seine Position und Abmessungen zu ändern. Dies erfordert ein wenig Mathematik, ist aber nicht allzu schwierig.
  • Play (Abspielen): Mit dem umgekehrten Element (das sich an der ersten Position befindet) können wir es durch Setzen seines transform auf none zurück in seine letzte Position bewegen.

Unten sehen Sie, wie diese Schritte mit der Web Animations API implementiert werden können

const elm = document.querySelector('.some-element');

// First: get the current bounds
const first = elm.getBoundingClientRect();

// execute the script that causes layout change
doSomething();

// Last: get the final bounds
const last = elm.getBoundingClientRect();

// Invert: determine the delta between the 
// first and last bounds to invert the element
const deltaX = first.left - last.left;
const deltaY = first.top - last.top;
const deltaW = first.width / last.width;
const deltaH = first.height / last.height;

// Play: animate the final element from its first bounds
// to its last bounds (which is no transform)
elm.animate([{
  transformOrigin: 'top left',
  transform: `
    translate(${deltaX}px, ${deltaY}px)
    scale(${deltaW}, ${deltaH})
  `
}, {
  transformOrigin: 'top left',
  transform: 'none'
}], {
  duration: 300,
  easing: 'ease-in-out',
  fill: 'both'
});

Hinweis: Zum Zeitpunkt des Schreibens wird die Web Animations API noch nicht in allen Browsern unterstützt. Sie können jedoch einen Polyfill verwenden.

Sehen Sie sich den Pen How the FLIP technique works von David Khourshid (@davidkpiano) auf CodePen an.

Es gibt zwei wichtige Dinge zu beachten

  1. Wenn sich die Größe des Elements geändert hat, können Sie scale transformieren, um es ohne Leistungseinbußen zu „vergrößern“; stellen Sie jedoch sicher, dass Sie transformOrigin auf 'top left' setzen, da dort unsere Delta-Berechnungen basieren.
  2. Wir verwenden hier die Web Animations API, um das Element zu animieren, aber Sie können jede andere Animations-Engine verwenden, z. B. GSAP, Anime, Velocity, Just-Animate, Mo.js und mehr.

Shared Element Transitions (Übergänge mit gemeinsam genutzten Elementen)

Ein häufiger Anwendungsfall für den Übergang eines Elements zwischen App-Ansichten und -Zuständen ist, dass das endgültige Element möglicherweise nicht dasselbe DOM-Element wie das anfängliche Element ist. Unter Android ist dies einem Shared Element Transition ähnlich, außer dass das Element nicht wie unter Android von Ansicht zu Ansicht im DOM „wiederverwendet“ wird.
Nichtsdestotrotz können wir den FLIP-Übergang mit ein wenig Magie Illusion erreichen

const firstElm = document.querySelector('.first-element');

// First: get the bounds and then hide the element (if necessary)
const first = firstElm.getBoundingClientRect();
firstElm.style.setProperty('visibility', 'hidden');

// execute the script that causes view change
doSomething();

// Last: get the bounds of the element that just appeared
const lastElm = document.querySelector('.last-element');
const last = lastElm.getBoundingClientRect();

// continue with the other steps, just as before.
// remember: you're animating the lastElm, not the firstElm.

Unten sehen Sie ein Beispiel dafür, wie zwei völlig unterschiedliche Elemente mit Shared Element Transitions wie dasselbe Element aussehen können. Klicken Sie auf eines der Bilder, um den Effekt zu sehen.

Sehen Sie sich den Pen FLIP example with WAAPI von David Khourshid (@davidkpiano) auf CodePen an.

Parent-Child Transitions (Eltern-Kind-Übergänge)

Bei den bisherigen Implementierungen basieren die Elementgrenzen auf dem window. Für die meisten Anwendungsfälle ist das in Ordnung, aber betrachten Sie dieses Szenario

  • Ein Element ändert seine Position und muss übergehen.
  • Dieses Element enthält ein Kindelement, das sich selbst innerhalb des übergeordneten Elements an eine andere Position bewegen muss.

Da die zuvor berechneten Grenzen relativ zum window sind, sind unsere Berechnungen für das Kindelement falsch. Um dies zu lösen, müssen wir sicherstellen, dass die Grenzen stattdessen relativ zum übergeordneten Element berechnet werden

const parentElm = document.querySelector('.parent');
const childElm = document.querySelector('.parent > .child');

// First: parent and child
const parentFirst = parentElm.getBoundingClientRect();
const childFirst = childElm.getBoundingClientRect();

doSomething();

// Last: parent and child
const parentLast = parentElm.getBoundingClientRect();
const childLast = childElm.getBoundingClientRect();

// Invert: parent
const parentDeltaX = parentFirst.left - parentLast.left;
const parentDeltaY = parentFirst.top - parentLast.top;

// Invert: child relative to parent
const childDeltaX = (childFirst.left - parentFirst.left)
  - (childLast.left - parentLast.left);
const childDeltaY = (childFirst.top - parentFirst.top)
  - (childLast.top - parentLast.top);
  
// Play: using the WAAPI
parentElm.animate([
  { transform: `translate(${parentDeltaX}px, ${parentDeltaY}px)` },
  { transform: 'none' }
], { duration: 300, easing: 'ease-in-out' });

childElm.animate([
  { transform: `translate(${childDeltaX}px, ${childDeltaY}px)` },
  { transform: 'none' }
], { duration: 300, easing: 'ease-in-out' });

Hier gibt es auch ein paar Dinge zu beachten

  1. Die Timing-Optionen für das übergeordnete und das untergeordnete Element (duration, easing usw.) müssen bei dieser Technik nicht unbedingt übereinstimmen. Seien Sie kreativ!
  2. Die Änderung von Abmessungen im übergeordneten und/oder untergeordneten Element (width, height) wurde in diesem Beispiel bewusst weggelassen, da dies ein fortgeschrittenes und komplexes Thema ist. Das heben wir uns für ein anderes Tutorial auf.
  3. Sie können die Techniken für Shared Elements und Parent-Child kombinieren, um größere Flexibilität zu erzielen.

Verwendung von Flipping.js für volle Flexibilität

Die oben genannten Techniken mögen einfach erscheinen, aber sie können ziemlich mühsam zu codieren sein, sobald Sie mehrere sich übergangsweise ändernde Elemente verfolgen müssen. Android erleichtert diese Last durch

  • integrierte Shared Element Transitions im Kern-SDK
  • ermöglicht Entwicklern, gemeinsam genutzte Elemente durch die Verwendung eines gemeinsamen Attributs android:transitionName in XML zu identifizieren

Ich habe eine kleine Bibliothek namens Flipping.js mit der gleichen Idee erstellt. Durch Hinzufügen eines data-flip-key="..." Attributs zu HTML-Elementen ist es möglich, Elemente, die ihre Position und Abmessungen von Zustand zu Zustand ändern, vorhersagbar und effizient zu verfolgen.

Betrachten Sie zum Beispiel diese anfängliche Ansicht

    <section class="gallery">

<div class="photo-1" data-flip-key="photo-1">
        <img src="/photo-1">
</div>


<div class="photo-2" data-flip-key="photo-2">
        <img src="/photo-2">
</div>


<div class="photo-3" data-flip-key="photo-3">
        <img src="/photo-3">
</div>

    </section>

Und diese separate Detailansicht

    <section class="details">

<div class="photo" data-flip-key="photo-1">
        <img src="/photo-1">
</div>

      
        Lorem ipsum dolor sit amet...
      
    
</section>

Beachten Sie im obigen Beispiel, dass es 2 Elemente mit demselben data-flip-key="photo-1" gibt. Flipping.js verfolgt das „aktive“ Element, indem es das erste Element auswählt, das diese Kriterien erfüllt

  • Das Element existiert im DOM (d. h. es wurde nicht entfernt oder getrennt)
  • Das Element ist nicht versteckt (Hinweis: elm.getBoundingClientRect() hat für versteckte Elemente { width: 0, height: 0 })
  • Jede benutzerdefinierte Logik, die in der Option selectActive angegeben ist.

Erste Schritte mit Flipping.js

Es gibt ein paar verschiedene Pakete für Flipping, je nach Ihren Bedürfnissen

  • flipping.js: winzig und Low-Level; gibt nur Ereignisse aus, wenn sich Elementgrenzen ändern
  • flipping.web.js: verwendet WAAPI zur Animation von Übergängen
  • flipping.gsap.js: verwendet GSAP zur Animation von Übergängen
  • Weitere Adapter folgen in Kürze!

Sie können den minifizierten Code direkt von unpkg abrufen

Oder Sie können npm install flipping --save ausführen und es in Ihre Projekte importieren

// import not necessary when including the unpkg scripts in a <script src="..."> tag
import Flipping from 'flipping/adapters/web';

const flipping = new Flipping();

// First: let Flipping read all initial bounds
flipping.read();

// execute the change that causes any elements to change bounds
doSomething();

// Last, Invert, Play: the flip() method does it all
flipping.flip();

Die Behandlung von FLIP-Übergängen als Ergebnis eines Funktionsaufrufs ist ein so gängiges Muster, dass die Methode .wrap(fn) die gegebene Funktion transparent umschließt (oder „dekoriert“), indem sie zuerst .read() aufruft, dann den Rückgabewert der Funktion abruft, dann .flip() aufruft und schließlich den Rückgabewert zurückgibt. Dies führt zu deutlich weniger Code

const flipping = new Flipping();

const flippingDoSomething = flipping.wrap(doSomething);

// anytime this is called, FLIP will animate changed elements
flippingDoSomething();

Hier ist ein Beispiel für die Verwendung von flipping.wrap(), um den Effekt von verschiebbaren Buchstaben einfach zu erzielen. Klicken Sie irgendwo, um den Effekt zu sehen.

Sehen Sie sich den Pen Flipping Birthstones #Codevember von David Khourshid (@davidkpiano) auf CodePen an.

Flipping.js zu bestehenden Projekten hinzufügen

In einem anderen Artikel haben wir eine einfache React-Galerie-App mit endlichen Zustandsautomaten erstellt. Sie funktioniert wie erwartet, aber die Benutzeroberfläche könnte einige flüssige Übergänge zwischen den Zuständen vertragen, um „Sprünge“ zu vermeiden und die Benutzererfahrung zu verbessern. Fügen wir Flipping.js in unsere React-App ein, um dies zu erreichen. (Beachten Sie, dass Flipping.js Framework-agnostisch ist.)

Schritt 1: Flipping.js initialisieren

Die Flipping-Instanz wird auf der React-Komponente selbst leben, damit sie auf Änderungen beschränkt ist, die innerhalb dieser Komponente auftreten. Initialisieren Sie Flipping.js, indem Sie es im componentDidMount Lifecycle Hook einrichten

  componentDidMount() {
    const { node } = this;
    if (!node) return;
    
    this.flipping = new Flipping({
      parentElement: node
    });
    
    // initialize flipping with the initial bounds
    this.flipping.read();
  }

Durch die Angabe von parentElement: node weisen wir Flipping an, nur nach Elementen mit einem data-flip-key in der gerenderten App zu suchen, anstatt im gesamten Dokument.
Ändern Sie dann die HTML-Elemente mit dem Attribut data-flip-key (ähnlich dem React-key-Prop), um eindeutige und „gemeinsame“ Elemente zu identifizieren

  renderGallery(state) {
    return (
      <section className="ui-items" data-state={state}>
        {this.state.items.map((item, i) =>
          <img
            src={item.media.m}
            className="ui-item"
            style={{'--i': i}}
            key={item.link}
            onClick={() => this.transition({
              type: 'SELECT_PHOTO', item
            })}
            data-flip-key={item.link}
          />
        )}
      </section>
    );
  }
  renderPhoto(state) {
    if (state !== 'photo') return;
    
    return (
      <section
        className="ui-photo-detail"
        onClick={() => this.transition({ type: 'EXIT_PHOTO' })}>
        <img
          src={this.state.photo.media.m}
          className="ui-photo"
          data-flip-key={this.state.photo.link}
        />
      </section>
    )
  }

Beachten Sie, wie img.ui-item und img.ui-photo durch data-flip-key={item.link} bzw. data-flip-key={this.state.photo.link} repräsentiert werden: Wenn der Benutzer auf ein img.ui-item klickt, wird dieser item zu this.state.photo gesetzt, sodass die .link-Werte gleich sind.

Und da sie gleich sind, wird Flipping sanft vom img.ui-item-Thumbnail zum größeren img.ui-photo übergehen.

Jetzt müssen wir noch zwei Dinge tun

  1. this.flipping.read() aufrufen, wann immer die Komponente aktualisiert *wird*
  2. this.flipping.flip() aufrufen, wann immer die Komponente aktualisiert *wurde*

Einige von Ihnen haben vielleicht schon erraten, wo diese Methodenaufrufe stattfinden werden: componentWillUpdate und componentDidUpdate, bzw.

  componentWillUpdate() {
    this.flipping.read();
  }
  
  componentDidUpdate() {
    this.flipping.flip();
  }

Und so wird Flipping, wenn Sie einen Flipping-Adapter verwenden (wie flipping.web.js oder flipping.gsap.js), alle Elemente mit einem [data-flip-key] verfolgen und sie sanft zu ihren neuen Grenzen überleiten, wann immer sie sich ändern. Hier ist das Endergebnis

Sehen Sie sich den Pen FLIPping Gallery App von David Khourshid (@davidkpiano) auf CodePen an.

Wenn Sie lieber benutzerdefinierte Animationen selbst implementieren möchten, können Sie flipping.js als einfachen Event-Emitter verwenden. Lesen Sie die Dokumentation für fortgeschrittenere Anwendungsfälle.

Flipping.js und seine Adapter behandeln standardmäßig Shared Element- und Parent-Child-Übergänge sowie

  • unterbrochene Übergänge (in Adaptern)
  • Enter/Move/Leave-Zustände
  • Plugin-Unterstützung für Plugins wie mirror, das neu eingegebenen Elementen erlaubt, die Bewegung eines anderen Elements zu „spiegeln“
  • und mehr für die Zukunft geplant!

Ressourcen

Ähnliche Bibliotheken umfassen

  • FlipJS von Paul Lewis selbst, das einfache Single-Element-FLIP-Übergänge handhabt
  • React-Flip-Move, eine nützliche React-Bibliothek von Josh Comeau
  • BarbaJS, nicht unbedingt eine FLIP-Bibliothek, aber eine, die es Ihnen ermöglicht, reibungslose Übergänge zwischen verschiedenen URLs hinzuzufügen, ohne Seitenumbrüche.

Weitere Ressourcen