建構高效展開和素材資源;收合動畫

Paul Lewis
史蒂芬麥克格瑞
Stephen McGruer

重點摘要

為短片加上動畫效果時使用縮放轉換功能。如要防止子項在動畫期間拉長或偏移,可以對子項進行計數器縮放。

我們先前已張貼更新內容,說明如何建立高效能的視差效果無限捲動工具。本文將詳細說明如何製作高效短片動畫。如要查看示範,請參閱 範例 UI 元素 GitHub 存放區

以展開式選單為例:

有部分選項會成效較其他選項來得好。

不佳:在容器元素上建立寬度和高度動畫

您可以想像用一些 CSS 為容器元素的寬度和高度建立動畫。

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

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

這個方法的立即問題在於需要為 widthheight 建立動畫效果。這些屬性需要計算版面配置,並在動畫的每個影格上繪製結果。這可能會耗費大量資源,而且通常會導致您錯過 60fps 的作業。最新消息,請參閱轉譯效能指南,進一步瞭解轉譯程序的運作方式。

不佳:使用 CSS 裁剪或裁剪路徑屬性

除了使用 widthheight 建立動畫之外,您也可以使用 clip 屬性 (現已淘汰) 為展開和收合效果製作動畫。或者,您也可以改用 clip-path。但是,與 clip 相比,使用 clip-path受到全面支援。但 clip 已淘汰。沒錯。但請別擔心,這不是您預期的解決方案!

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

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

儘管對選單元素的 widthheight 建立動畫效果更好,但此方法的缺點是仍可觸發繪製作業。此外,如果路徑為 clip 屬性,則該路徑所執行的元素必須指定絕對位置或固定位置,這可能會需要多一點疊加。

良好:為體重計加上動畫效果

此效果需要放大及縮小,因此您可以使用比例轉換。這才是好消息,因為改變的轉換不需要版面配置或繪製,而瀏覽器可以向 GPU 發出,也就是說,效果會加速,且更有可能達到 60fps。

但這個方法的缺點,如同大多數轉譯效能的缺點,就是需要進行一些設定。但這絕對值得!

步驟 1:計算開始與結束狀態

使用縮放動畫的方法時,首先要讀取元素,瞭解選單在收合和展開時所需的大小。在某些情況下,可能無法一氣呵成這些資訊,而您必須視情況切換某些類別,以便讀取元件的各種狀態。但是,如果您需要這麼做,請注意:getBoundingClientRect() (或 offsetWidthoffsetHeight) 會在樣式自上次執行後變更樣式時,強制瀏覽器執行樣式和版面配置傳遞。

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

對於選單之類的項目,我們可以做出合理假設,讓其一開始會以自然的尺度 (1, 1) 開始顯示。此自然比例代表展開狀態,也就是說,您必須從縮減版本 (如上述計算) 建立動畫,一直到該自然縮放比例。

可是,這麼做可以調整選單內容,對吧?如下方所示,沒錯

該怎麼辦?您可以將 counter- 轉換套用至內容,因此舉例來說,如果容器縮減為正常大小的 1/5,您可以將內容「向上」調整為 5 倍,以免內容遭到壓縮。請留意以下兩點:

  1. 計數器轉換也是比例作業。由於也可以像容器上的動畫一樣加速,因此這是良好。您可能需要確保動畫的元素取得專屬的合成層 (可讓 GPU 提供協助),並為元素新增 will-change: transform;如果需要支援舊版瀏覽器,則 backface-visiblity: hidden

  2. 必須按影格計算計數器轉換。這可能會變得較複雜,因為假設動畫是採用 CSS 格式,而且使用加/減速函式,就要為計數器轉換設定動畫效果時,必須反轉加/減速設定。不過,假設 cubic-bezier(0, 0, 0.3, 1) 並非顯而易見,例如:

你可能會想使用 JavaScript 以動畫效果呈現效果。之後,您就可以使用加/減速公式計算每影格的比例和計數器尺度值。任何以 JavaScript 為基礎的動畫缺點,是當主執行緒 (JavaScript 執行時) 處理其他工作時,會發生什麼事。簡單來說,您的動畫可能會延遲或完全停止,因為這並不適合使用者體驗。

步驟 2:即時建構 CSS 動畫

但一開始可能會產生奇怪的解決方法,就是使用自有的加/減速函式建立主要畫面格動畫,並將其插入頁面供選單使用。(非常感謝 Chrome 工程師 Robert Flack 特別指出這一點!)這麼做的主要好處是,能夠改變轉換的主要畫面格動畫可以在合成器上執行,也就是說,其不受主執行緒中的工作影響。

如要製作主要畫面格動畫,我們會逐步從 0 開始至 100,並計算元素及其內容所需的縮放值。接著,這些字串可繫結至字串,以樣式元素插入頁面中。插入樣式會導致「重新計算樣式」傳遞到頁面上,這是瀏覽器必須進行的額外工作,但系統只會在元件啟動時執行一次。

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

既然如此,您可能會想知道 for 迴圈中的 ease() 函式。您可以使用這類方式,將 0 到 1 的值對應到簡化的等值。

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

您也可以使用 Google 搜尋來繪製外觀。實用!如果您需要其他加/減速方程式,請查看 Tween.js by Soledad Penadés,其中包含整個堆積的堆積。

步驟 3:啟用 CSS 動畫

這些動畫在 JavaScript 中建立並製備至網頁後,最後一步是切換啟用動畫的類別。

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

這樣會執行上一個步驟中建立的動畫。由於現行的動畫已經簡化,時間函式必須設為 linear,否則每個主要畫面格看起來都很奇怪!

如要將元素向下收合,有兩種選項:更新 CSS 動畫以反向執行 (而非向前)。這樣雖然沒問題,但動畫的「資產」反而會反轉,因此如果您使用緩解曲線,反向操作會變得「in」,而會變得較為緩慢。更合適的做法是建立第二組動畫來收合元素。建立這些項目的方式與展開主要畫面格動畫相同,但會使用已切換的開始和結束值。

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

進階版本:循環顯示

你也可以使用這項技巧製作圓形展開及收合動畫。

這些原則大致與先前版本相同,也就是您針對元素進行縮放後,對其直接子項進行反轉。在此範例中,向上縮放的元素有 50% 的 border-radius,設為圓形,並由包含 overflow: hidden 的「另一個」元素包裝。也就是說,圓形在元素邊界外不會展開。

針對這個特定變化版本提供的警告說明:由於文字的比例和計數器縮放錯誤,導致 Chrome 在動畫播放期間的 DPI 螢幕出現模糊效果。如果您有興趣進一步瞭解相關資訊,請找出錯誤,並加上星號並加以追蹤

您可以在 GitHub 存放區中找到循環展開效果的程式碼。

結論

您已經完成了,這是使用縮放轉換功能執行高效剪輯動畫的方法。在完美的世界中,可以看到剪輯動畫加速 (這是由 Jake Archibald 所製作的 Chromium 錯誤) 一樣,但在此之前,您應謹慎為 clipclip-path 建立動畫,並絕對避免使用 widthheight 動畫。

此外,使用網路動畫產生這類效果也很有幫助,因為這類動畫具有 JavaScript API,但如果您只建立 transformopacity 的動畫,可以在合成執行緒上執行。遺憾的是,對網路動畫的支援不太完善,但您也可以採用漸進式增強功能 (如果有的話)。

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

在這些變更之前,雖然您可以使用以 JavaScript 為基礎的程式庫進行動畫,但可能會發現製作 CSS 動畫並改用這種動畫,可獲得更可靠的效能。同理,如果應用程式已使用 JavaScript 處理動畫,則建議至少與現有程式碼集保持一致,才能改善使用效果。

如果您想查看這種效果的程式碼,請查看 UI Element 範例 GitHub 存放區