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
作者:郑鱼咚
作者:郑鱼咚
欢迎关注微信公众号 :前端开发爱好者
添加好友备注【进阶学习】拉你进技术交流群