CSS3-animation+JS实现iphone14Pro“灵动岛”动画【附完整代码】

哈喽,大家好 我是xy。今天将给大家带来炫酷的 iphone 14Pro“灵动岛” 动画实践,帮助你梳理 web 动画实用知识
前言
首先,苹果的“灵动岛”设计确实巧妙。作为曾经的一位数码爱好者,最近几年确实很少在 UI 交互上看到这样令人眼前一亮的创新。

那一块挤满元器件的“感叹号”区域,虽然无法正常显示内容,但它完全能够做到可触控(屏幕的触控层与显示层是分离的),影响显示并不等于影响交互。这也体现了苹果设计师一贯的独立思考能力。这让笔者回忆起大学时期酷爱的那部魅族 mx2,当年的“小圆圈”设计也很精巧。只不过,苹果这次的设计更加大胆,动画也更加夸张,也更会包装起名字... 毕业之后,从事了前端工作,恰逢中秋佳节,北漂在外,闲来无事,尝试运用 CSS3-animation + JS 实现一个简易版本的“灵动岛”连播动画。

实现的最终效果如下,虽不及苹果官网的酷炫。但勉强也算以小见大、见微知著吧!在文章结尾,笔者会贴出完整的代码实现。但本文并不会以介绍具体实现为主,而是通过一些实现过程中的重点,梳理一些 web 动画方面的基础知识。毕竟中后台做久了,难免会忘记一些更偏 C 端的样式及动画知识,所以对自己而言也是一次难得的“温故而知新”的机会。



web 动画基础
1. CSS 与 JS 在动画实现上的边界
随着设备对css3的支持度越来越高,在大部分场景上完全能取代 js 来实现复杂且精美的动画效果。但同时也导致一些人在选择上的困惑: 同样的一个动画场景是使用 css3 还是 js 来实现呢?答案是:相互协同,取长补短。

由于 js 单线程的特性,天生不适合做大量的密集运算,所以用作动画过程的渲染时,常常会出现不流畅的效果。而这恰恰是 css3 的强项,尤其在给元素添加translateZ(0)开启 GPU 硬件加速后,在动画的绘制性能方面是明显强于 JS 的。JS 作为一门图灵完备的编程语言,它的强项在于对动画流程的控制。比如在实现“灵动岛”动画的连续播放时,纯 css3 的解决方案是:

.dynamic-island{
  ...
  animation: 动画1,动画2,动画3;
  ...
}
但这种仅仅能实现最简单的自动连续播放需求,但是想实现诸如:

通过一个点击事件触发播放;
整个动画组合循环轮播等等这些稍微复杂点的需求,纯 CSS 的方案就有局限了。那么这时就必须使用擅长逻辑控制的js,配合丰富的动画事件来实现:
   // 灵动岛对应dom
    const box = document.querySelector(".dynamic-island");
   // 以类名定义所有动画类型,以类名切换,实现动画切换
    const animationList = ["longer", "divide", "fusion", "bigger"];
    box.addEventListener("click", () => {
      box.classList.add(animationList[index]);
    });
    let index = 0;
    // 每一个动画结束都会触发此事件(包括子元素及不同属性动画结束时)
    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);
    });
总结:js擅长处理对动画的流程控制及基于事件的对整个动画过程的感知,css3则在动画渲染的性能及动画关键帧定义的便利性方面更有优势,适合用于动画过程的渲染。

2. transition 与 animation 的选择
苹果“灵动岛”的动画,更多的实际上可看作是一种“过渡”动画: 由元素的一种状态向另一种状态的过渡。所以我首先尝试的就是 transition 属性,但做出来总感觉差点意思,缺少一种所谓的“灵动感”。在仔细观看官网的动画细节后发现,这些动画在结尾部分常常表现出一种“超出边界继续放大,接着又往回收缩”的类似拉扯橡皮筋的效果



这是transiton无法实现的,所以果断换用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);
    }
  }
总结就是:transition只适用于元素两个状态间的切换(开始、结束),一旦所需切换状态超过两个,就需要用animation的百分比来定义中间的动画帧了。

3. JS 控制动画播放及切换的三种方式
切换 class 类名 (推荐)
box.classList.toggle('longer');
直接覆盖 animation 属性
box.style.animation = `longer 800ms ease-in-out`;
缺点是由于动画属性值较长,保存多个动画所需的字符串会较长,不如将动画属性封装在一个个的 css 类名下,通过切换类名来的简洁方便。

animationPlayState 属性
  box.style.animationPlayState="paused" // runing播放,paused暂停。
这里有关于此属性的介绍。但这种方式只适用于控制单个动画的播放状态,但对预期的“灵动岛”多个动画切换的场景,就明显不适用了。






3. 非线形动画
IOS 系统相比安卓原生采用的Material-Design在动画设计方面最显著的区别,就是大量采用了「非线性动画」。大白话解释就是,动画的速度不是恒定的,可能忽快忽慢。这项功能使用 css3 实现非常简单,通过定义CSS3 animation-timing-function 属性,即可完成。内置的几种属性值基本就可满足大部分需求,笔者采用的是ease-in-out慢进慢出的方式,这与苹果官网的效果接近,当然如果你不嫌麻烦,也可以通过自定义cubic-bezier(n,n,n,n)贝赛尔曲线函数来量身定制。这里也多说一句:动画的开发中,难的不是技术实现,而是动画细节的调整。快一点、慢一点对开发者来说也许就是一些参数的差别,但对优秀的设计师而言,1px 的差异、毫秒级别的快慢,也会影响整体的用户体验,甚至决定整个系统的“气质”。

4. 动画结束后如何让元素停留在结束时的状态
css 动画结束后,默认不会应用最后动画帧的元素状态,也就是会打回原形,这往往不符合需求,这里提供两种思路。

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

js 在动画结束时,主动查询一次 style 属性,并给 dom 重新赋值一遍 但是因为 dom 样式的查询会触发提前重绘,所以是极不推荐的方式,只用来处理一些特殊场景。

5. translate 与 postion 在实现位移上的区别
位移是最常见的动画场景,这两个属性均可实现。但两者还是有明显区别的,首先transform: translate 只是表现层面的位移,并不会实际影响 dom 的位置,所以它也不会触发重排等影响页面性能的行为。优点当然是性能好,但如果需要在动画过程中即时查询 dom 的offsetTop、offsetLeft等信息,采用postion去实现动画会是一个相对更加保险的方案。

完整代码如下:
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <title>灵动岛</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>
      // 灵动岛对应dom
      const box = document.querySelector(".dynamic-island");

      const animationList = ["longer", "divide", "fusion", "bigger"];
      box.addEventListener("click", () => {
        box.classList.add(animationList[index]);
      });
      let index = 0;
      // 每一个动画结束都会触发此事件(包括子元素及不同种类属性动画)
      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
作者:郑鱼咚


作者:郑鱼咚


欢迎关注微信公众号 :前端开发爱好者


添加好友备注【进阶学习】拉你进技术交流群