Leistungsstarke Animationen zum Maximieren und Minimieren erstellen

Paul Lewis
Stephen McGruer
Stephen McGruer

Kurzfassung

Verwende bei der Animation von Clips Skalierungstransformationen. Sie können verhindern, dass die Kinder während der Animation gedehnt und verzerrt werden, indem Sie sie gegenskalieren.

Wir haben bereits Informationen dazu veröffentlicht, wie Sie leistungsstarke Parallaxeeffekte und unendlich scrollbare Elemente erstellen. In diesem Beitrag erfährst du, was du beachten musst, wenn du leistungsstarke Clip-Animationen erstellen möchtest. Eine Demo finden Sie im GitHub-Repository für Beispiel-UI-Elemente.

Nehmen wir zum Beispiel ein ausziehbares Menü:

Einige Optionen für die Erstellung sind leistungsfähiger als andere.

Nicht empfohlen: Breite und Höhe eines Containerelements animieren

Sie könnten sich vorstellen, mit ein bisschen CSS die Breite und Höhe des Containerelements zu animieren.

.menu {
  overflow: hidden;
  width: 350px;
  height: 600px;
  transition: width 600ms ease-out, height 600ms ease-out;
}

.menu--collapsed {
  width: 200px;
  height: 60px;
}

Das unmittelbare Problem bei diesem Ansatz ist, dass width und height animiert werden müssen. Für diese Properties muss das Layout berechnet und das Ergebnis in jedem Frame der Animation gerendert werden. Das kann sehr aufwendig sein und führt in der Regel dazu, dass Sie keine 60 fps erreichen. Wenn Sie das noch nicht wussten, lesen Sie unsere Leitfäden zur Renderingleistung. Dort finden Sie weitere Informationen zum Ablauf des Renderings.

Nicht empfohlen: CSS-Attribute „clip“ oder „clip-path“ verwenden

Eine Alternative zum Animieren von width und height ist die Verwendung der (jetzt veralteten) Eigenschaft clip, um den Maximierungs- und Minimierungseffekt zu animieren. Sie können stattdessen auch clip-path verwenden. Die Verwendung von clip-path wird jedoch weniger gut unterstützt als clip. clip ist jedoch eingestellt. Aber keine Sorge, das ist sowieso nicht die Lösung, die du dir gewünscht hast.

.menu {
  position: absolute;
  clip: rect(0px 112px 175px 0px);
  transition: clip 600ms ease-out;
}

.menu--collapsed {
  clip: rect(0px 70px 34px 0px);
}

Das ist zwar besser als die Animation der width und height des Menüelements, aber es wird trotzdem noch ein Paint-Vorgang ausgelöst. Außerdem muss das Element, auf das die clip-Eigenschaft angewendet wird, entweder absolut oder fix positioniert sein, was etwas mehr Aufwand erfordern kann.

Gut: Skalen animieren

Da bei diesem Effekt etwas größer und kleiner wird, können Sie eine Skalierungstransformation verwenden. Das ist eine gute Nachricht, da für die Änderung von Transformationen kein Layout oder Painting erforderlich ist und der Browser die Aufgabe an die GPU weitergeben kann. Das bedeutet, dass der Effekt beschleunigt wird und die Wahrscheinlichkeit, dass 60 fps erreicht werden, deutlich höher ist.

Der Nachteil dieses Ansatzes ist, wie bei den meisten Dingen in Bezug auf die Renderingleistung, dass er etwas eingerichtet werden muss. Es lohnt sich aber!

Schritt 1: Start- und Endstatus berechnen

Bei einem Ansatz mit Skalierungsanimationen müssen Sie zuerst Elemente lesen, die angeben, wie groß das Menü sowohl im minimierten als auch im maximierten Zustand sein muss. In einigen Fällen ist es möglich, dass Sie diese beiden Informationen nicht gleichzeitig abrufen können und beispielsweise einige Klassen umschalten müssen, um die verschiedenen Status der Komponente lesen zu können. Wenn Sie dies tun müssen, seien Sie jedoch vorsichtig: getBoundingClientRect() (oder offsetWidth und offsetHeight) zwingt den Browser, Stile und Layoutpässe auszuführen, wenn sich die Stile seit der letzten Ausführung geändert haben.

function calculateCollapsedScale () {
    // The menu title can act as the marker for the collapsed state.
    const collapsed = menuTitle.getBoundingClientRect();

    // Whereas the menu as a whole (title plus items) can act as
    // a proxy for the expanded state.
    const expanded = menu.getBoundingClientRect();
    return {
    x: collapsed.width / expanded.width,
    y: collapsed.height / expanded.height
    };
}

Bei einem Menü können wir davon ausgehen, dass es zu Beginn im natürlichen Maßstab (1, 1) ist. Dieser natürliche Maßstab entspricht dem maximierten Zustand. Sie müssen also von einer verkleinerten Version (die oben berechnet wurde) wieder auf diesen natürlichen Maßstab animieren.

Aber, oh nein! Dadurch würde sich natürlich auch der Inhalt des Menüs skalieren, oder? Ja, wie Sie unten sehen können.

Was können Sie also tun? Sie können eine Gegentransformation auf die Inhalte anwenden. Wenn der Container beispielsweise auf ein Fünftel seiner normalen Größe skaliert wird, können Sie die Inhalte vergrößern, um zu verhindern, dass sie zu klein werden. Dabei gibt es zwei Dinge zu beachten:

  1. Die Umkehrtransformation ist auch ein Skalierungsvorgang. Das ist gut, weil es sich genau wie die Animation des Containers beschleunigen lässt. Möglicherweise müssen Sie dafür sorgen, dass die animierten Elemente eine eigene Kompositionsebene erhalten, damit die GPU unterstützen kann. Dazu können Sie dem Element will-change: transform oder, wenn Sie ältere Browser unterstützen müssen, backface-visiblity: hidden hinzufügen.

  2. Die Umkehrung muss pro Frame berechnet werden. Hier kann es etwas komplizierter werden, denn vorausgesetzt, die Animation ist in CSS und verwendet eine Ease-Funktion, muss die Ease-Funktion selbst bei der Animation der Gegentransformation aufgehoben werden. Die Berechnung der inversen Kurve für beispielsweise cubic-bezier(0, 0, 0.3, 1) ist jedoch nicht ganz so einfach.

Es kann verlockend sein, den Effekt mit JavaScript zu animieren. Schließlich können Sie dann mit einer Ease-Gleichung die Skalierungs- und Gegenskalierungswerte pro Frame berechnen. Der Nachteil jeder JavaScript-basierten Animation ist, was passiert, wenn der Hauptthread (in dem Ihr JavaScript ausgeführt wird) mit einer anderen Aufgabe beschäftigt ist. Die kurze Antwort ist, dass Ihre Animation stottern oder ganz anhalten kann, was nicht gut für die UX ist.

Schritt 2: CSS-Animationen in Echtzeit erstellen

Die Lösung, die auf den ersten Blick etwas seltsam erscheinen mag, besteht darin, eine Keyframe-Animation mit unserer eigenen Ease-Funktion dynamisch zu erstellen und sie für das Menü in die Seite einzufügen. (Vielen Dank an den Chrome-Entwickler Robert Flack für diesen Hinweis!) Der Hauptvorteil besteht darin, dass eine Keyframe-Animation, die Transformationen verändert, im Compositor ausgeführt werden kann. Sie ist also nicht von Aufgaben im Hauptthread betroffen.

Für die Keyframe-Animation gehen wir von 0 bis 100 und berechnen, welche Skalierungswerte für das Element und seinen Inhalt erforderlich sind. Diese können dann auf einen String reduziert werden, der als Stilelement in die Seite eingefügt werden kann. Wenn Sie die Stile einfügen, wird auf der Seite ein Befehl zum Neuberechnen der Stile ausgeführt. Das ist eine zusätzliche Arbeit, die der Browser erledigen muss, aber nur einmal, wenn die Komponente gestartet wird.

function createKeyframeAnimation () {
    // Figure out the size of the element when collapsed.
    let {x, y} = calculateCollapsedScale();
    let animation = '';
    let inverseAnimation = '';

    for (let step = 0; step <= 100; step++) {
    // Remap the step value to an eased one.
    let easedStep = ease(step / 100);

    // Calculate the scale of the element.
    const xScale = x + (1 - x) * easedStep;
    const yScale = y + (1 - y) * easedStep;

    animation += `${step}% {
        transform: scale(${xScale}, ${yScale});
    }`;

    // And now the inverse for the contents.
    const invXScale = 1 / xScale;
    const invYScale = 1 / yScale;
    inverseAnimation += `${step}% {
        transform: scale(${invXScale}, ${invYScale});
    }`;

    }

    return `
    @keyframes menuAnimation {
    ${animation}
    }

    @keyframes menuContentsAnimation {
    ${inverseAnimation}
    }`;
}

Die unendlich neugierigen Leser fragen sich vielleicht, was die ease()-Funktion in der For-Schleife bedeutet. So können Sie Werte von 0 bis 1 einem geglätteten Äquivalent zuordnen.

function ease (v, pow=4) {
  return 1 - Math.pow(1 - v, pow);
}

Sie können sich das auch in der Google Suche ansehen. Praktisch! Wenn Sie weitere Glättegleichungen benötigen, sehen Sie sich Tween.js von Soledad Penadés an, das eine ganze Reihe davon enthält.

Schritt 3: CSS-Animationen aktivieren

Nachdem diese Animationen erstellt und in JavaScript auf die Seite eingebettet wurden, besteht der letzte Schritt darin, Klassen zu aktivieren, die die Animationen ermöglichen.

.menu--expanded {
  animation-name: menuAnimation;
  animation-duration: 0.2s;
  animation-timing-function: linear;
}

.menu__contents--expanded {
  animation-name: menuContentsAnimation;
  animation-duration: 0.2s;
  animation-timing-function: linear;
}

Dadurch werden die im vorherigen Schritt erstellten Animationen ausgeführt. Da die gebackenen Animationen bereits eine Überblendung haben, muss die Timing-Funktion auf linear festgelegt werden. Andernfalls wird zwischen jedem Keyframe eine Überblendung angewendet, was sehr seltsam aussehen würde.

Wenn Sie das Element wieder minimieren möchten, haben Sie zwei Möglichkeiten: Sie können die CSS-Animation so aktualisieren, dass sie rückwärts statt vorwärts ausgeführt wird. Das funktioniert zwar, aber die Animation wirkt dann rückwärts. Wenn Sie also eine „Ease-Out“-Kurve verwendet haben, wird die Rückwärtsbewegung verzögert, was sich träge anfühlt. Eine geeignetere Lösung besteht darin, ein zweites Paar von Animationen zum Minimieren des Elements zu erstellen. Diese können genau wie die Animations-Keyframes für das Auseinanderziehen erstellt werden, jedoch mit vertauschten Start- und Endwerten.

const xScale = 1 + (x - 1) * easedStep;
const yScale = 1 + (y - 1) * easedStep;

Eine erweiterte Version: kreisförmige Enthüllungen

Mit dieser Technik lassen sich auch kreisförmige Animationen zum Maximieren und Minimieren erstellen.

Die Prinzipien sind weitgehend dieselben wie bei der vorherigen Version, bei der Sie ein Element skalieren und seine unmittelbaren untergeordneten Elemente in die entgegengesetzte Richtung skalieren. In diesem Fall hat das Element, das skaliert wird, eine border-radius von 50%, was es kreisförmig macht. Es ist von einem anderen Element umgeben, das overflow: hidden hat. Das bedeutet, dass der Kreis nicht über die Elementgrenzen hinaus erweitert wird.

Hinweis zu dieser Variante: Auf Bildschirmen mit niedriger Auflösung ist der Text in Chrome während der Animation verschwommen, da es aufgrund der Skalierung und der Gegenskalierung des Texts zu Rundungsfehlern kommt. Wenn du mehr darüber erfahren möchtest, kannst du diesem Fehlerbericht ein Sternchen hinzufügen und ihm folgen.

Den Code für den kreisförmigen Ausdehnungseffekt finden Sie im GitHub-Repository.

Ergebnisse

So kannst du mithilfe von Skalierungstransformationen leistungsstarke Clip-Animationen erstellen. In einer perfekten Welt wären Clip-Animationen beschleunigt. Es gibt dazu einen Chromium-Bug von Jake Archibald. Bis dahin sollten Sie bei der Animation von clip oder clip-path vorsichtig sein und width oder height auf keinen Fall animieren.

Für solche Effekte eignen sich auch Web-Animationen, da sie eine JavaScript-API haben, aber im Compositor-Thread ausgeführt werden können, wenn Sie nur transform und opacity animieren. Leider ist die Unterstützung für Webanimationen nicht sehr gut. Sie können sie jedoch mithilfe der progressiven Verbesserung verwenden, sofern sie verfügbar sind.

if ('animate' in HTMLElement.prototype) {
    // Animate with Web Animations.
} else {
    // Fall back to generated CSS Animations or JS.
}

Bis dahin können Sie zwar JavaScript-basierte Bibliotheken für die Animation verwenden, aber Sie erzielen möglicherweise eine zuverlässigere Leistung, wenn Sie eine CSS-Animation erstellen und stattdessen verwenden. Wenn Ihre App bereits JavaScript für ihre Animationen verwendet, ist es möglicherweise besser, wenn Sie zumindest mit Ihrer vorhandenen Codebasis konsistent bleiben.

Wenn Sie sich den Code für diesen Effekt ansehen möchten, sehen Sie sich das GitHub-Repository mit UI-Element-Beispielen an. Wie immer freuen wir uns über Ihre Kommentare.