sunilwang

V1

2022/09/21阅读:11主题:全栈蓝

从一次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;
  ...
}

但这种仅仅能实现最简单的自动连续播放需求,但是想实现诸如:(1)通过一个点击事件触发播放; (2)整个动画组合循环轮播等等这些稍微复杂点的需求,纯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 属性,但做出来总感觉差点意思,缺少一种所谓的“灵动感”。在仔细观看官网的动画细节后发现,这些动画在结尾部分常常表现出一种“超出边界继续放大,接着又往回收缩”的类似拉扯橡皮筋的效果,如下GIF所示。

这是transiton无法实现的,所以果断换用animation

  @keyframes bigger {
    0% {
    }
    60% {
      width81vw;
      height400px;
      border-radius100px;
    }
    80% {
      transformscaleX(1.04);
    }
    100% {
      width81vw;
      height400px;
      border-radius100px;
      transformscaleX(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>
      * {
        margin0;
        padding0;
      }
      #iphone14pro {
        position: relative;
        margin: auto;
        width974px;
        height876px;
        overflow: hidden;
        background-imageurl(https://www.apple.com.cn/v/iphone-14-pro/a/images/overview/dynamic-island/dynamic_hw__btl4fomgspyu_large.png);
      }
      .dynamic-island {
        width320px;
        margin-top72px;
        margin72px auto 0;
        background-color: red;
        height80px;
        border-radius40px;
        background-color#272729;
        position: relative;
      }
      .dynamic-island::after {
        position: absolute;
        content" ";
        right0;
        width80px;
        height100%;
        border-radius80px;
        background-color#272729;
      }
      /* 变长 */
      .longer {
        animation: longer 800ms ease-in-out forwards;
      }
      @keyframes longer {
        0% {
        }
        60% {
          width50vw;
        }
        80% {
          transformscaleX(1.04);
        }
        100% {
          transformscaleX(1);
          width50vw;
        }
      }
      /* 分离 */
      .divide {
        animation: divide-left 800ms ease-in-out forwards;
      }
      @keyframes divide-left {
        0% {
        }
        40% {
          transformscaleX(1.1);
        }

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

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

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

        100% {
          transformscaleX(1);
          right0;
        }
      }
      /* 变大 */
      .bigger {
        animation: bigger 800ms ease-in-out forwards;
      }
      @keyframes bigger {
        0% {
        }
        60% {
          width81vw;
          height400px;
          border-radius100px;
        }
        80% {
          transformscaleX(1.04);
        }
        100% {
          width81vw;
          height400px;
          border-radius100px;
          transformscaleX(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>
作者简介

郑瑜栋:在团队拥有一个奇怪的外号“=”哥。

分类:

前端

标签:

CSS

作者介绍

sunilwang
V1