本文源自我的公眾號: 實(shí)踐requestAnimationFrame平滑動畫
背景
前端領(lǐng)域?qū)崿F(xiàn)動畫效果通常有這么幾種方式: css animation悍赢、setTimeout(setInterval)、Lottie玄妈。
css animation 動畫是通過關(guān)鍵幀(@keyframes) 來實(shí)現(xiàn)的,優(yōu)點(diǎn)是寫法比較簡單笨触,缺點(diǎn)就是難以獲取動畫的開始和結(jié)束事件丛版。
setTimeout 一般通過callback 來控制元素的變化實(shí)現(xiàn)動畫的穿剖,但是定時器動畫一直存在兩個問題超埋,第一個就是動畫的循時間環(huán)間隔不好確定哲思,設(shè)置長了動畫顯得不夠平滑流暢洼畅,設(shè)置短了瀏覽器的重繪頻率會達(dá)到瓶頸,推薦的最佳循環(huán)間隔是17ms(大多數(shù)電腦的顯示器刷新頻率是60Hz棚赔,1000ms/60)帝簇;第二個問題是定時器第二個時間參數(shù)只是指定了多久后將動畫任務(wù)添加到瀏覽器的UI線程隊(duì)列中徘郭,如果UI線程處于忙碌狀態(tài),那么動畫不會立刻執(zhí)行丧肴。
lottie 一般是使用 canvas 或者 svg 方式來實(shí)現(xiàn)動畫的残揉, 通過引入配置文件來實(shí)現(xiàn)。但是復(fù)雜動畫會導(dǎo)致配置文件過大芋浮,進(jìn)而導(dǎo)致webpack打包體積過于龐大抱环。
本次要實(shí)現(xiàn)能量球的 軌跡運(yùn)動 動畫 及 總能量縮放 動畫,以上的方案實(shí)踐下來均不能實(shí)現(xiàn)平滑的效果纸巷。后來了解到 H5 中加入了 requestAnimationFrame镇草,該方法是根據(jù)瀏覽器的刷新頻率來執(zhí)行回調(diào)方法,可以很好的控制動畫的開始和結(jié)束事件何暇。
尷尬陶夜,簡書上傳不了視頻凛驮,如果看效果裆站,請移步到公眾號文章地址查看。 https://mp.weixin.qq.com/s/yTex4ewF0cbG1tluaqxKzw
說明
window.requestAnimationFrame()
告訴瀏覽器——你希望執(zhí)行一個動畫黔夭,并且要求瀏覽器在下次重繪之前調(diào)用指定的回調(diào)函數(shù)更新動畫宏胯。該方法需要傳入一個回調(diào)函數(shù)作為參數(shù),該回調(diào)函數(shù)會在瀏覽器下一次重繪之前執(zhí)行本姥。
注意:若你想在瀏覽器下次重繪之前繼續(xù)更新下一幀動畫肩袍,那么回調(diào)函數(shù)自身必須再次調(diào)用
window.requestAnimationFrame()
當(dāng)你準(zhǔn)備更新動畫時你應(yīng)該調(diào)用此方法。這將使瀏覽器在下一次重繪之前調(diào)用你傳入給該方法的動畫函數(shù)(即你的回調(diào)函數(shù))婚惫》沾停回調(diào)函數(shù)執(zhí)行次數(shù)通常是每秒60次,但在大多數(shù)遵循W3C建議的瀏覽器中先舷,回調(diào)函數(shù)執(zhí)行次數(shù)通常與瀏覽器屏幕刷新次數(shù)相匹配艰管。為了提高性能和電池壽命,因此在大多數(shù)瀏覽器里蒋川,當(dāng)requestAnimationFrame()
運(yùn)行在后臺標(biāo)簽頁或者隱藏的iframe
里時牲芋,requestAnimationFrame()
會被暫停調(diào)用以提升性能和電池壽命。
回調(diào)函數(shù)會被傳入DOMHighResTimeStamp
參數(shù)捺球,DOMHighResTimeStamp
指示當(dāng)前被 requestAnimationFrame()
排序的回調(diào)函數(shù)被觸發(fā)的時間缸浦。在同一個幀中的多個回調(diào)函數(shù),它們每一個都會接受到一個相同的時間戳氮兵,即使在計(jì)算上一個回調(diào)函數(shù)的工作負(fù)載期間已經(jīng)消耗了一些時間裂逐。該時間戳是一個十進(jìn)制數(shù),單位毫秒泣栈,最小精度為1ms(1000μs)絮姆。
請確弊碓總是使用第一個參數(shù)(或其它獲得當(dāng)前時間的方法)計(jì)算每次調(diào)用之間的時間間隔,否則動畫在高刷新率的屏幕中會運(yùn)行得更快篙悯。
語法
window.requestAnimationFrame(callback);
參數(shù)
callback
下一次重繪之前更新動畫幀所調(diào)用的函數(shù)(即上面所說的回調(diào)函數(shù))蚁阳。該回調(diào)函數(shù)會被傳入
DOMHighResTimeStamp
參數(shù),該參數(shù)與performance.now()
的返回值相同鸽照,它表示requestAnimationFrame()
開始去執(zhí)行回調(diào)函數(shù)的時刻螺捐。
返回值
一個 long
整數(shù),請求 ID 矮燎,是回調(diào)列表中唯一的標(biāo)識定血。是個非零值,沒別的意義诞外。你可以傳這個值給 window.cancelAnimationFrame()
以取消回調(diào)函數(shù)澜沟。
范例
以下代碼根據(jù)回調(diào)的時間戳傳參(說明:這里是timestamp 表示為從time origin之后到當(dāng)前調(diào)用時經(jīng)過的時間),確保不同刷新頻率的屏幕 都可以在 兩秒內(nèi)停止動畫峡谊。
const element = document.getElementById('some-element-you-want-to-animate');
let start;
function step(timestamp) {
if (start === undefined)
start = timestamp;
const elapsed = timestamp - start;
//這里使用`Math.min()`確保元素剛好停在200px的位置茫虽。
element.style.transform = 'translateX(' + Math.min(0.1 * elapsed, 200) + 'px)';
if (elapsed < 2000) { // 在兩秒后停止動畫
window.requestAnimationFrame(step);
}
}
window.requestAnimationFrame(step);
實(shí)踐
本次實(shí)踐收能量平滑動畫不考慮高刷新頻率的屏幕,暫定為用戶使用的都是 一秒鐘 60 刷新頻次的屏幕既们。動畫分三部分:向下動畫濒析、軌跡動畫、縮放動畫啥纸。
- 下滑動畫
收取能量球開始有個向下的動畫号杏,每次下滑2px,能量球下滑到 60 px 的時候(下滑動畫執(zhí)行了30次斯棒,按照一秒鐘刷新60次盾致,動畫大約執(zhí)行0.5秒),則執(zhí)行向右上角的軌跡動畫荣暮,用 requestAnimationFrame 可以這么實(shí)現(xiàn):
// 點(diǎn)擊能量球事件
clickBall() {
this.dropY = 0;
this.perY = 2;
requestAnimationFrame(this.dropBall.bind(this));
}
// 下滑軌跡動畫
dropBall() {
if(this.dropY < 60) {
this.dropY += this.perY;
this.ballRef.current.style.top = `${this.props.style.top + this.dropY}px`;
requestAnimationFrame(this.dropBall.bind(this));
} else {
// 軌跡動畫
this.parabola();
}
}
- 軌跡動畫
軌跡動畫庭惜,則通過 getBoundingClientRect
方法 分別獲取 開始元素(能量球當(dāng)前位置)坐標(biāo) 和 結(jié)束元素(總能量位置)坐標(biāo)的位置, 進(jìn)而計(jì)算出需要運(yùn)動的距離渠驼。根據(jù)需要的時間和每秒60次的刷新頻率蜈块,計(jì)算出 每次 translate 的距離即可。(可以參照張鑫旭實(shí)踐的軌跡動畫迷扇,此處不列舉實(shí)現(xiàn)方式)百揭。
- 縮放動畫
最后是總能量的縮放動畫(這里實(shí)現(xiàn)的縮放是 從1.0 放大到1.3, 然后再縮回到1.0):
// 總能量收取事件 及 觸發(fā)動畫效果
collectPower({energyNum}) {
this.totalPower += energyNum;
this.powerRef.current.innerText = this.totalPower;
this.minuteFlag = false;
this.scale = 1.0;
this.perAdd = 0.02;
requestAnimationFrame(this.scalePower.bind(this));
}
// 動畫效果
scalePower() {
if (this.scale < 1.3 && !this.minuteFlag) {
this.scale += this.perAdd;
this.powerRef.current.style.transform = `scale(${this.scale})`;
requestAnimationFrame(this.scalePower.bind(this));
} else if (this.scale > 1) {
this.minuteFlag = true;
this.scale -= this.perAdd;
this.powerRef.current.style.transform = `scale(${this.scale})`;
requestAnimationFrame(this.scalePower.bind(this));
} else {
this.powerRef.current.style.transform = `scale(1)`;
}
}