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ù)交流群