CSS3-animation+JS實(shí)現(xiàn)iphone14Pro“靈動(dòng)島”動(dòng)畫【附完整代碼】

哈嘍,大家好 我是xy。今天將給大家?guī)盱趴岬?iphone 14Pro“靈動(dòng)島” 動(dòng)畫實(shí)踐,幫助你梳理 web 動(dòng)畫實(shí)用知識(shí)
前言
首先,蘋果的“靈動(dòng)島”設(shè)計(jì)確實(shí)巧妙。作為曾經(jīng)的一位數(shù)碼愛好者,最近幾年確實(shí)很少在 UI 交互上看到這樣令人眼前一亮的創(chuàng)新。

那一塊擠滿元器件的“感嘆號(hào)”區(qū)域,雖然無法正常顯示內(nèi)容,但它完全能夠做到可觸控(屏幕的觸控層與顯示層是分離的),影響顯示并不等于影響交互。這也體現(xiàn)了蘋果設(shè)計(jì)師一貫的獨(dú)立思考能力。這讓筆者回憶起大學(xué)時(shí)期酷愛的那部魅族 mx2,當(dāng)年的“小圓圈”設(shè)計(jì)也很精巧。只不過,蘋果這次的設(shè)計(jì)更加大膽,動(dòng)畫也更加夸張,也更會(huì)包裝起名字... 畢業(yè)之后,從事了前端工作,恰逢中秋佳節(jié),北漂在外,閑來無事,嘗試運(yùn)用 CSS3-animation + JS 實(shí)現(xiàn)一個(gè)簡易版本的“靈動(dòng)島”連播動(dòng)畫。

實(shí)現(xiàn)的最終效果如下,雖不及蘋果官網(wǎng)的酷炫。但勉強(qiáng)也算以小見大、見微知著吧!在文章結(jié)尾,筆者會(huì)貼出完整的代碼實(shí)現(xiàn)。但本文并不會(huì)以介紹具體實(shí)現(xiàn)為主,而是通過一些實(shí)現(xiàn)過程中的重點(diǎn),梳理一些 web 動(dòng)畫方面的基礎(chǔ)知識(shí)。畢竟中后臺(tái)做久了,難免會(huì)忘記一些更偏 C 端的樣式及動(dòng)畫知識(shí),所以對(duì)自己而言也是一次難得的“溫故而知新”的機(jī)會(huì)。



web 動(dòng)畫基礎(chǔ)
1. CSS 與 JS 在動(dòng)畫實(shí)現(xiàn)上的邊界
隨著設(shè)備對(duì)css3的支持度越來越高,在大部分場景上完全能取代 js 來實(shí)現(xiàn)復(fù)雜且精美的動(dòng)畫效果。但同時(shí)也導(dǎo)致一些人在選擇上的困惑: 同樣的一個(gè)動(dòng)畫場景是使用 css3 還是 js 來實(shí)現(xiàn)呢?答案是:相互協(xié)同,取長補(bǔ)短。

由于 js 單線程的特性,天生不適合做大量的密集運(yùn)算,所以用作動(dòng)畫過程的渲染時(shí),常常會(huì)出現(xiàn)不流暢的效果。而這恰恰是 css3 的強(qiáng)項(xiàng),尤其在給元素添加translateZ(0)開啟 GPU 硬件加速后,在動(dòng)畫的繪制性能方面是明顯強(qiáng)于 JS 的。JS 作為一門圖靈完備的編程語言,它的強(qiáng)項(xiàng)在于對(duì)動(dòng)畫流程的控制。比如在實(shí)現(xiàn)“靈動(dòng)島”動(dòng)畫的連續(xù)播放時(shí),純 css3 的解決方案是:

.dynamic-island{
  ...
  animation: 動(dòng)畫1,動(dòng)畫2,動(dòng)畫3;
  ...
}
但這種僅僅能實(shí)現(xiàn)最簡單的自動(dòng)連續(xù)播放需求,但是想實(shí)現(xiàn)諸如:

通過一個(gè)點(diǎn)擊事件觸發(fā)播放;
整個(gè)動(dòng)畫組合循環(huán)輪播等等這些稍微復(fù)雜點(diǎn)的需求,純 CSS 的方案就有局限了。那么這時(shí)就必須使用擅長邏輯控制的js,配合豐富的動(dòng)畫事件來實(shí)現(xiàn):
   // 靈動(dòng)島對(duì)應(yīng)dom
    const box = document.querySelector(".dynamic-island");
   // 以類名定義所有動(dòng)畫類型,以類名切換,實(shí)現(xiàn)動(dòng)畫切換
    const animationList = ["longer", "divide", "fusion", "bigger"];
    box.addEventListener("click", () => {
      box.classList.add(animationList[index]);
    });
    let index = 0;
    // 每一個(gè)動(dòng)畫結(jié)束都會(huì)觸發(fā)此事件(包括子元素及不同屬性動(dòng)畫結(jié)束時(shí))
    box.addEventListener("animationend", (e) => {
      if (
        e.animationName === "divide-right" ||
        e.animationName === "fusion-right"
      ) {
        return;
      }
      index++;
      setTimeout(() => {
        if (index <= animationList.length - 1) {
          box.classList.add(animationList[index]);
        } else {
          index = 0;
        }
      }, 800);
    });
總結(jié):js擅長處理對(duì)動(dòng)畫的流程控制及基于事件的對(duì)整個(gè)動(dòng)畫過程的感知,css3則在動(dòng)畫渲染的性能及動(dòng)畫關(guān)鍵幀定義的便利性方面更有優(yōu)勢,適合用于動(dòng)畫過程的渲染。

2. transition 與 animation 的選擇
蘋果“靈動(dòng)島”的動(dòng)畫,更多的實(shí)際上可看作是一種“過渡”動(dòng)畫: 由元素的一種狀態(tài)向另一種狀態(tài)的過渡。所以我首先嘗試的就是 transition 屬性,但做出來總感覺差點(diǎn)意思,缺少一種所謂的“靈動(dòng)感”。在仔細(xì)觀看官網(wǎng)的動(dòng)畫細(xì)節(jié)后發(fā)現(xiàn),這些動(dòng)畫在結(jié)尾部分常常表現(xiàn)出一種“超出邊界繼續(xù)放大,接著又往回收縮”的類似拉扯橡皮筋的效果



這是transiton無法實(shí)現(xiàn)的,所以果斷換用animation。

  @keyframes bigger {
    0% {
    }
    60% {
      width: 81vw;
      height: 400px;
      border-radius: 100px;
    }
    80% {
      transform: scaleX(1.04);
    }
    100% {
      width: 81vw;
      height: 400px;
      border-radius: 100px;
      transform: scaleX(1);
    }
  }
總結(jié)就是:transition只適用于元素兩個(gè)狀態(tài)間的切換(開始、結(jié)束),一旦所需切換狀態(tài)超過兩個(gè),就需要用animation的百分比來定義中間的動(dòng)畫幀了。

3. JS 控制動(dòng)畫播放及切換的三種方式
切換 class 類名 (推薦)
box.classList.toggle('longer');
直接覆蓋 animation 屬性
box.style.animation = `longer 800ms ease-in-out`;
缺點(diǎn)是由于動(dòng)畫屬性值較長,保存多個(gè)動(dòng)畫所需的字符串會(huì)較長,不如將動(dòng)畫屬性封裝在一個(gè)個(gè)的 css 類名下,通過切換類名來的簡潔方便。

animationPlayState 屬性
  box.style.animationPlayState="paused" // runing播放,paused暫停。
這里有關(guān)于此屬性的介紹。但這種方式只適用于控制單個(gè)動(dòng)畫的播放狀態(tài),但對(duì)預(yù)期的“靈動(dòng)島”多個(gè)動(dòng)畫切換的場景,就明顯不適用了。






3. 非線形動(dòng)畫
IOS 系統(tǒng)相比安卓原生采用的Material-Design在動(dòng)畫設(shè)計(jì)方面最顯著的區(qū)別,就是大量采用了「非線性動(dòng)畫」。大白話解釋就是,動(dòng)畫的速度不是恒定的,可能忽快忽慢。這項(xiàng)功能使用 css3 實(shí)現(xiàn)非常簡單,通過定義CSS3 animation-timing-function 屬性,即可完成。內(nèi)置的幾種屬性值基本就可滿足大部分需求,筆者采用的是ease-in-out慢進(jìn)慢出的方式,這與蘋果官網(wǎng)的效果接近,當(dāng)然如果你不嫌麻煩,也可以通過自定義cubic-bezier(n,n,n,n)貝賽爾曲線函數(shù)來量身定制。這里也多說一句:動(dòng)畫的開發(fā)中,難的不是技術(shù)實(shí)現(xiàn),而是動(dòng)畫細(xì)節(jié)的調(diào)整。快一點(diǎn)、慢一點(diǎn)對(duì)開發(fā)者來說也許就是一些參數(shù)的差別,但對(duì)優(yōu)秀的設(shè)計(jì)師而言,1px 的差異、毫秒級(jí)別的快慢,也會(huì)影響整體的用戶體驗(yàn),甚至決定整個(gè)系統(tǒng)的“氣質(zhì)”。

4. 動(dòng)畫結(jié)束后如何讓元素停留在結(jié)束時(shí)的狀態(tài)
css 動(dòng)畫結(jié)束后,默認(rèn)不會(huì)應(yīng)用最后動(dòng)畫幀的元素狀態(tài),也就是會(huì)打回原形,這往往不符合需求,這里提供兩種思路。

animation-fill-mode:forwards;(推薦)

js 在動(dòng)畫結(jié)束時(shí),主動(dòng)查詢一次 style 屬性,并給 dom 重新賦值一遍 但是因?yàn)?dom 樣式的查詢會(huì)觸發(fā)提前重繪,所以是極不推薦的方式,只用來處理一些特殊場景。

5. translate 與 postion 在實(shí)現(xiàn)位移上的區(qū)別
位移是最常見的動(dòng)畫場景,這兩個(gè)屬性均可實(shí)現(xiàn)。但兩者還是有明顯區(qū)別的,首先transform: translate 只是表現(xiàn)層面的位移,并不會(huì)實(shí)際影響 dom 的位置,所以它也不會(huì)觸發(fā)重排等影響頁面性能的行為。優(yōu)點(diǎn)當(dāng)然是性能好,但如果需要在動(dòng)畫過程中即時(shí)查詢 dom 的offsetTop、offsetLeft等信息,采用postion去實(shí)現(xiàn)動(dòng)畫會(huì)是一個(gè)相對(duì)更加保險(xiǎn)的方案。

完整代碼如下:
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <title>靈動(dòng)島</title>
    <style>
      * {
        margin: 0;
        padding: 0;
      }
      #iphone14pro {
        position: relative;
        margin: auto;
        width: 974px;
        height: 876px;
        overflow: hidden;
        background-image: url(https://www.apple.com.cn/v/iphone-14-pro/a/images/overview/dynamic-island/dynamic_hw__btl4fomgspyu_large.png);
      }
      .dynamic-island {
        width: 320px;
        margin-top: 72px;
        margin: 72px auto 0;
        background-color: red;
        height: 80px;
        border-radius: 40px;
        background-color: #272729;
        position: relative;
      }
      .dynamic-island::after {
        position: absolute;
        content: " ";
        right: 0;
        width: 80px;
        height: 100%;
        border-radius: 80px;
        background-color: #272729;
      }
      /* 變長 */
      .longer {
        animation: longer 800ms ease-in-out forwards;
      }
      @keyframes longer {
        0% {
        }
        60% {
          width: 50vw;
        }
        80% {
          transform: scaleX(1.04);
        }
        100% {
          transform: scaleX(1);
          width: 50vw;
        }
      }
      /* 分離 */
      .divide {
        animation: divide-left 800ms ease-in-out forwards;
      }
      @keyframes divide-left {
        0% {
        }
        40% {
          transform: scaleX(1.1);
        }

        100% {
          transform: scaleX(1);
        }
      }
      .divide::after {
        animation: divide-right 800ms ease-in-out forwards;
      }
      @keyframes divide-right {
        0% {
        }
        40% {
          transform: scaleX(1.1);
        }

        100% {
          transform: scaleX(1);
          right: -100px;
        }
      }
      /* 融合 */
      .fusion {
        animation: fusion-left 800ms ease-in-out forwards;
      }
      @keyframes fusion-left {
        0% {
        }
        40% {
          transform: scaleX(1.1);
        }

        100% {
          transform: scaleX(1);
        }
      }
      .fusion::after {
        animation: fusion-right 800ms ease-in-out forwards;
      }
      @keyframes fusion-right {
        0% {
          right: -100px;
        }
        40% {
          transform: scaleX(1.1);
        }

        100% {
          transform: scaleX(1);
          right: 0;
        }
      }
      /* 變大 */
      .bigger {
        animation: bigger 800ms ease-in-out forwards;
      }
      @keyframes bigger {
        0% {
        }
        60% {
          width: 81vw;
          height: 400px;
          border-radius: 100px;
        }
        80% {
          transform: scaleX(1.04);
        }
        100% {
          width: 81vw;
          height: 400px;
          border-radius: 100px;
          transform: scaleX(1);
        }
      }
      .bigger::after {
        display: none;
      }
    </style>
  </head>
  <body>
    <div id="iphone14pro">
      <div class="dynamic-island"></div>
    </div>
    <script>
      // 靈動(dòng)島對(duì)應(yīng)dom
      const box = document.querySelector(".dynamic-island");

      const animationList = ["longer", "divide", "fusion", "bigger"];
      box.addEventListener("click", () => {
        box.classList.add(animationList[index]);
      });
      let index = 0;
      // 每一個(gè)動(dòng)畫結(jié)束都會(huì)觸發(fā)此事件(包括子元素及不同種類屬性動(dòng)畫)
      box.addEventListener("animationend", (e) => {
        if (
          e.animationName === "divide-right" ||
          e.animationName === "fusion-right"
        ) {
          return;
        }
        index++;
        setTimeout(() => {
          if (index <= animationList.length - 1) {
            box.classList.add(animationList[index]);
          } else {
            index = 0;
          }
        }, 800);
      });
    </script>
  </body>
</html>
原文鏈接:https://juejin.cn/post/7142412129520812046
作者:鄭魚咚


作者:鄭魚咚


歡迎關(guān)注微信公眾號(hào) :前端開發(fā)愛好者


添加好友備注【進(jìn)階學(xué)習(xí)】拉你進(jìn)技術(shù)交流群