Tworzenie wydajnych animacji rozwijania i zwijania

Stefan McGruer
Stephen McGruer

TL;DR

Stosuj przekształcenia skali podczas animowania klipów. Możesz zapobiec rozciąganiu i zniekształceniu elementów podrzędnych podczas animacji, skalując je z poziomu licznika.

Wcześniej publikowaliśmy informacje o sposobach tworzenia skutecznych efektów paralaksy i przewijania nieskończonego. W tym poście omówimy, co warto zrobić, jeśli potrzebujesz skutecznych animacji. Jeśli chcesz zobaczyć prezentację, zajrzyj do repozytorium na GitHubie z przykładowymi elementami interfejsu użytkownika.

Przykładowe menu rozwijane:

Niektóre sposoby tworzenia tej funkcji są skuteczniejsze niż inne.

Błąd: animowanie szerokości i wysokości elementu kontenera

Wyobraź sobie, że używasz CSS do animowania szerokości i wysokości elementu kontenera.

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

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

Bezpośredni problem z tym podejściem polega na tym, że wymaga ono animacji width i height. Właściwości te wymagają obliczenia układu i wyświetlania wyników w każdej klatce animacji, co bywa bardzo kosztowne i zwykle powoduje utratę wartości 60 kl./s. Jeśli to dla Ciebie nowość, przeczytaj nasze przewodniki po wydajności renderowania, w których znajdziesz więcej informacji o procesie renderowania.

Nieprawidłowo: użyj właściwości klipu CSS lub clip-path

Zamiast animować właściwości width i height, możesz użyć (obecnie wycofanej) właściwości clip, aby animować efekt rozwijania i zwijania. Możesz też użyć atrybutu clip-path. Jednak użycie clip-path jest mniej obsługiwane niż clip. Ale właściwość clip została wycofana. W prawo. Nie martw się, to nie jest rozwiązanie, o które Ci chodziło!

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

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

Chociaż jest to lepsze niż animowanie elementów width i height elementu menu, wadą tej metody jest to, że nadal wywołuje renderowanie. Ponadto właściwość clip, jeśli pójdziesz tą drogą, wymaga, aby element, na którym działa, znajdował się w pozycji bezwzględnej lub stałej, co może wymagać dodatkowego zniekształcenia.

Dobrze: animowanie waga

Ponieważ ten efekt wymaga powiększania i pomniejszania elementów, możesz użyć przekształcenia skali. To świetna wiadomość, bo wprowadzanie zmian w transformacjach nie wymaga układu ani malowania, a przeglądarka może przekazać do GPU, co oznacza, że efekt jest przyspieszony i znacznie zwiększa prawdopodobieństwo osiągnięcia 60 kl./s.

Wadą tego podejścia, jak w przypadku większości problemów z wydajnością renderowania, jest to, że wymaga ono nieco konfiguracji. Jednak naprawdę warto.

Krok 1. Oblicz stan początkowy i końcowy

W przypadku animacji skalowania pierwszym krokiem jest odczytanie elementów, które informują, że menu musi mieć rozmiar zarówno w przypadku zwiniętego, jak i rozwiniętego menu. W niektórych sytuacjach nie można jednocześnie uzyskać obu tych informacji i trzeba na przykład przełączać klasy, żeby odczytywać różne stany komponentu. Jeśli jednak musisz to zrobić, zachowaj ostrożność: atrybuty getBoundingClientRect() (lub offsetWidth i offsetHeight) wymuszają na przeglądarce uruchamianie stylów i przekazań układu, jeśli styl uległy zmianie od czasu ostatniego uruchomienia.

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
    };
}

W przypadku czegoś takiego jak menu możemy przyjąć rozsądne założenie, że zaczyna się ono w swojej naturalnej skali (1, 1). Ta naturalna skala reprezentuje stan rozwinięty, co oznacza, że trzeba będzie animować wersję zmniejszoną (obliczoną powyżej) z powrotem do tej skali.

Ale zaraz! Z pewnością zwiększy to też zawartość menu, prawda? Jak widać poniżej, tak.

Co możesz zrobić w tej sytuacji? Do zawartości możesz zastosować przekształcenie counter-. Jeśli np. kontener jest skalowany w dół do 1/5 normalnego rozmiaru, możesz 5-krotnie przeskalować zawartość counter-, aby zapobiec jej zgnieszczeniu. Warto zwrócić uwagę na 2 rzeczy:

  1. Przekształcenie licznika również jest operacją skalowania. Jest dobra, bo można ją też przyspieszyć, podobnie jak animacja w kontenerze. Być może elementy, które mają być animowane, mają własną warstwę kompozytora (umożliwiającą obsługę GPU). W tym celu dodaj do elementu atrybut will-change: transform lub, jeśli chcesz obsługiwać starsze przeglądarki, backface-visiblity: hidden.

  2. Przekształcenie licznika należy obliczać dla każdej klatki. Tutaj wszystko może być nieco trudniejsze, bo zakładając, że animacja jest w języku CSS i używasz funkcji wygładzania, podczas animowania przekształcenia licznika trzeba zrównoważyć z tym samym wygładzanie. Jednak obliczenie odwrotnej krzywej dla – na przykład cubic-bezier(0, 0, 0.3, 1) – nie jest takie oczywiste.

W takiej sytuacji animowanie efektów za pomocą JavaScriptu może być kuszące. Można by użyć równania wygładzania do obliczania wartości skali i licznika skalowania w każdej klatce. Wadą każdej animacji opartej na JavaScripcie jest to, co dzieje się, gdy wątek główny (w którym działa JavaScript) jest zajęty innym zadaniem. Krótko mówiąc, animacja może się zacinać lub całkowicie się zatrzymać, co nie jest dobre dla UX.

Krok 2. Twórz na bieżąco animacje CSS

Rozwiązaniem, które na początku może wydawać się dziwne, jest utworzenie animacji z klatkami kluczowymi z własną funkcją wygładzania i wstawienie jej na stronie do użycia w menu. (Wielkie podziękowania dla inżyniera Chrome za zwrócenie uwagi na ten problem!) Główną korzyścią jest to, że animacja z klatkami kluczowymi, która mutuje przekształcenia, może być uruchamiana w kompozytorze, co oznacza, że zadania w wątku głównym nie mają na nią wpływu.

Aby utworzyć animację klatki kluczowej, przechodzimy od 0 do 100 i obliczamy, jakie wartości skali byłyby potrzebne dla elementu i jego zawartości. Dane te można potem skończyć w postaci ciągu tekstowego, który można wstawić na stronie jako element stylu. Wstrzyknięcie stylów spowoduje przekazanie stylów ponownie obliczonych na stronie, co jest dodatkową pracą, którą musi wykonać przeglądarka, ale zrobi to tylko raz podczas uruchamiania komponentu.

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}
    }`;
}

Osoby nieskończenie dociekliwe mogą zastanawiać się nad funkcją ease() w pętli for. Możesz użyć tego polecenia, aby zmapować wartości od 0 do 1 na odpowiedniki w prostym stylu.

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

Aby zobaczyć, jak to wygląda, możesz też użyć wyszukiwarki Google. Przydatne. Jeśli szukasz innych równań wygładzania, zajrzyj do narzędzia Tween.js firmy Soledad Penadés, gdzie znajdziesz ich całą masę.

Krok 3. Włącz animacje CSS

Po utworzeniu i wypaleniu na stronie animacji JavaScriptu ostatnim krokiem jest przełączenie klas umożliwiających korzystanie z animacji.

.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;
}

Powoduje to uruchomienie animacji utworzonych w poprzednim kroku. Upieczone animacje są już wygładzone, więc funkcja czasu musi być ustawiona na linear. W przeciwnym razie będzie to efekt wygładzania klatek kluczowych, co będzie wyglądać bardzo dziwnie.

Masz do wyboru 2 możliwości zwinięcia elementu z powrotem: zaktualizowanie animacji CSS tak, aby poruszała się w odwrotnym tempie, a nie do przodu. Będzie to proste, ale „odczucie” animacji zostanie odwrócone. Jeśli więc użyjesz krzywej wygładzania, na odwrót będzie ona wygładzana, przez co będzie wolna. Bardziej odpowiednim rozwiązaniem jest utworzenie drugiej pary animacji do zwijania elementu. Można je tworzyć dokładnie tak samo jak animacje rozwijanych klatek kluczowych, ale z zamiennymi wartościami początkowymi i końcowymi.

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

Bardziej zaawansowana wersja: ujawnianie cyklicznie

Tej metody możesz też używać do tworzenia animacji zwijania i rozwijania na bieżąco.

Zasady są w dużej mierze takie same jak w poprzedniej wersji, w której skalujesz element i skalujesz jego najbliższe elementy podrzędne z przeciwieństwem. W tym przypadku element skalujący się w górę ma wartość border-radius równą 50%, przez co jest okrągły, i otacza go inny element z parametrem overflow: hidden. Oznacza to, że okrąg nie rozwija się poza granice elementu.

Uwaga dotycząca tego konkretnego wariantu: w czasie animacji Chrome ma rozmyty tekst na ekranach o niskiej rozdzielczości DPI z powodu błędów zaokrąglania spowodowanych skalą i licznikiem skali tekstu. Jeśli chcesz dowiedzieć się więcej na ten temat, znajdź błąd, który możesz oznaczyć gwiazdką i obserwować.

Kod efektu rozwijania kołowego znajdziesz w repozytorium GitHub.

Podsumowanie

I o to chodzi, oto jak stworzyć efektywne animacje klipu za pomocą przekształceń skali. W idealnym świecie byłoby przyspieszenie animacji klipów (w tym przypadku był to błąd Chromium stworzony przez Jake'a Archibalda), ale dopóki tego nie dotrzemy, zachowaj ostrożność podczas animowania clip lub clip-path i zdecydowanie unikaj animowania width czy height.

Do takich efektów warto też użyć animacji internetowych, ponieważ zawierają one interfejs API JavaScript, ale można je uruchamiać w wątku kompozytora, jeśli animujesz tylko transform i opacity. Niestety, animacje internetowe nie są obsługiwane, chociaż można by ich używać z pomocą progresywnego ulepszania, jeśli są dostępne.

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

Do tego czasu, chociaż możesz używać bibliotek opartych na JavaScript do tworzenia animacji, może się okazać, że bardziej niezawodną wydajność uzyskasz, wypalając animację CSS i używając jej zamiast niej. Jeśli Twoja aplikacja korzysta już z JavaScriptu do tworzenia animacji, ich wyświetlanie może być korzystne, jeśli Twoja aplikacja powinna być co najmniej zgodna z dotychczasową bazą kodu.

Jeśli chcesz zapoznać się z kodem tego efektu, zajrzyj do repozytorium UI Element Samples na GitHubie i jak zawsze daj nam znać w komentarzach poniżej, jak Ci poszło.