Leistungsstarke Animationen zum Maximieren und Minimieren erstellen

Paul Lewis
Stephen McGruer
Stephen McGruer

Kurzfassung

Verwende bei der Animation von Clips Skalierungstransformationen. Durch eine Gegenskalierung können Sie verhindern, dass die untergeordneten Elemente während der Animation gestreckt und verzerrt werden.

Zuvor haben wir Updates zum Erstellen leistungsstarker Parallaxe-Effekte und unendlicher Scroller veröffentlicht. 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 UI-Elemente-Beispiele.

Nehmen wir als Beispiel ein maximiertes Menü:

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

Nicht empfohlen: Breite und Höhe eines Containerelements animieren

Um Breite und Höhe des Containerelements zu animieren, können Sie sich die CSS vorstellen.

.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 besteht darin, dass width und height animiert werden müssen. Für diese Eigenschaften muss das Layout berechnet werden und die Ergebnisse müssen auf jeden Frame der Animation übertragen werden. Das kann sehr teuer sein und dazu führen, dass Ihnen 60 fps entgehen. Wenn Sie das noch nicht wussten, lesen Sie unsere Leitfäden zur Renderingleistung. Dort finden Sie weitere Informationen zum Ablauf des Renderings.

Nicht empfohlen: Die 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 Sie sich gewünscht haben.

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

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

Diese Methode ist besser als die Animation von width und height des Menüelements. Der Nachteil dieses Ansatzes ist jedoch, dass weiterhin Paint ausgelöst wird. 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 sind gute Neuigkeiten, da bei Transformationen kein Layout oder Paint erforderlich sind und der Browser an die GPU übergeben kann. Dies bedeutet, dass der Effekt beschleunigt wird und mit deutlich höherer Wahrscheinlichkeit 60 fps erreicht.

Der Nachteil dieses Ansatzes ist, wie bei den meisten Aspekten der Rendering-Leistung, dass eine gewisse Einrichtung erforderlich ist. Es lohnt sich aber auf jeden Fall!

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 auf einmal 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! Sicher würde dadurch auch der Inhalt des Menüs skaliert werden, 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 um das Fünffache vergrößern, um zu verhindern, dass sie zu klein werden. Dabei sind zwei Dinge zu beachten:

  1. Die Umkehrtransformation ist auch ein Skalierungsvorgang. Dies ist gut, da es ebenso wie die Animation auf dem Container beschleunigt werden kann. 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.

Daher kann es 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. Kurz gesagt: Ihre Animation kann ruckeln oder ganz anhalten, was für UX nicht gut 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. Dies 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 unter Ihnen 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 das Ergebnis auch über die Google-Suche grafisch darstellen. Praktisch! Wenn Sie andere Easing-Gleichungen benötigen, sehen Sie sich Tween.js von Soledad Penadés an, das einen ganzen Heap 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 problemlos, aber das "Anschauungsgefühl" der Animation wird umgekehrt. Wenn Sie eine ease-out-Kurve verwendet haben, wirkt sich dies also in weicher an, wodurch sie sich langsam 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 Anfangs- und Endwerten.

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

Eine komplexere Version: kreisförmige Darstellungen zeigen

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 hochskaliert wird, ein border-radius von 50%, d. h. es ist kreisförmig und wird von einem weiteren Element mit overflow: hidden umschlossen. Das bedeutet, dass der Kreis nicht außerhalb der Elementgrenzen erweitert wird.

Ein Hinweis zu dieser speziellen Variante: In Chrome wird der Text auf Bildschirmen mit niedrigem DPI-Wert während der Animation aufgrund von Rundungsfehlern aufgrund der Größe des Textes verschwommen dargestellt. 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.

Schlussfolgerungen

So kannst du mithilfe von Skalierungstransformationen leistungsstarke Clip-Animationen erstellen. In einer perfekten Welt wäre es toll, wenn Clipanimationen beschleunigt würden (dafür gibt es einen Chromium-Fehler von Jake Archibald). Bis wir so weit sind, solltet ihr bei der Animation von clip oder clip-path vorsichtig sein und auf keinen Fall width oder height 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. Gleichermaßen sollten Sie, wenn Ihre App bereits auf JavaScript für Animationen angewiesen ist, mindestens eine Konsistenz mit Ihrer vorhandenen Codebasis bieten.

Wenn Sie sich den Code für diesen Effekt ansehen möchten, können Sie einen Blick auf das GitHub-Repository für UI-Elemente-Beispiele werfen. Teilen Sie uns wie immer in den Kommentaren unten mit, wie Sie zurechtkommen.