自己做個(gè) Material Ripple 效果的按鈕
本文已獲得原作者的獨(dú)家授權(quán),有想轉(zhuǎn)載的朋友們可以在后臺(tái)聯(lián)系我申請開白哦! PS:歡迎掘友們向我投稿哦,被采用的文章還可以送你掘金精美周邊!
背景介紹

我感覺他挺好看的!
我第一次發(fā)現(xiàn) Material Design 是幾年前玩 Android(當(dāng)時(shí)還不會(huì)開發(fā) Android 應(yīng)用程序)時(shí)候看到的些貼文。那時(shí)候我就超級喜歡它的按鈕組件。它有著波紋效果,以簡單,優(yōu)雅的方式為用戶提供反饋,Q 彈爆汁兒~
那時(shí)候的我也只會(huì)使用固定的 :hover :focus 樣式,效果固定而死板,那是我這種一班人用的,Google 那群二班的真的太強(qiáng)了!??!

你看看這圓潤的外框,這活潑的顏色 ♂?,這似乎汁水四溢的效果,是不是像極了你們欠我的那個(gè)贊 :)

我們可以完全做到一樣的效果!
需求一覽
Ripple 效果 自動(dòng)為所有元素加效果 監(jiān)聽新元素的插入
該咋辦?
我打算用 JavaScript 監(jiān)聽點(diǎn)擊事件,向按鈕添加子元素(Ripple 動(dòng)效元素),并向按鈕添加 .ripple 類,并監(jiān)聽 DOM 樹中的變化,如果有 .ripple 元素的加入,就為其綁定 Ripple 效果。
stateDiagram-v2
[*] --> 按鈕事件
按鈕事件 --> 未綁定
按鈕事件 --> 已綁定
未綁定 --> 綁定按鈕
綁定按鈕 --> 動(dòng)效
已綁定 --> 動(dòng)效
動(dòng)效 --> 添加 ripple
添加 ripple --> 添加子元素
添加子元素 --> [*]
HTML
<button>一個(gè)簡簡單單的按鈕</button>
CSS
對于 Ripple 效果,我們會(huì)等下直接用 JavaScript 去動(dòng)態(tài)設(shè)置,而樣式的定義,就在如下的一些代碼中解決:
button {
position: relative;
overflow: hidden;
}
使用 position: relative 允許我們等下構(gòu)造的子元素針對按鈕本體能夠使用 position: absolute。同時(shí),overflow: hidden 可以幫助我們防止 Ripple 效果超出按鈕的輪廓。然后再裝飾一下:
/* 用上 Material 的默認(rèn)字體 */
@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');
button {
position: relative; /* 下文中會(huì)用到的相對絕對位置 */
overflow: hidden;
transition: background 400ms ease-in-out; /* 設(shè)置切換 */
color: #fff;
background-color: #662D91;
padding: 1rem 2rem;
font-family: 'Roboto', sans-serif;
outline: 0;
border: 0;
border-radius: 0.25rem;
box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.2);
cursor: pointer;
}
現(xiàn)在它是這樣的:

Ripple
Ripple 效果實(shí)際上就是一個(gè)半徑不斷擴(kuò)展的標(biāo)準(zhǔn)圓,而被沿著按鈕外框裁切掉。因此我們先來繪制一個(gè)標(biāo)準(zhǔn)圓:
span.ripple {
position: absolute; /* 上文中我們提到過的相對絕對位置 */
border-radius: 50%;
transform: scale(0);
animation: ripple 600ms linear;
background-color: rgba(255, 255, 255, 0.2);
}
為了使波紋變圓,我們設(shè)置 border-radius 為 50%。而為了確保動(dòng)畫開始時(shí)候沒有效果,我們設(shè)置了默認(rèn)縮放比例 0?,F(xiàn)在,我們將無法看到任何東西,因?yàn)槲覀冞€沒有設(shè)置 top、left、width 以及 height,也沒有修改默認(rèn)縮放比例 transform: scale(0)。不用著急,馬上我們就會(huì)用 JavaScript 設(shè)置這些屬性!
現(xiàn)在我們還需要給 Ripple 效果添加動(dòng)畫切換,就讓它縮放到 4 倍大小吧:
@keyframes ripple {
to {
transform: scale(4);
opacity: 0;
}
}
JavaScript
現(xiàn)在我們需要使用 JavaScript 來動(dòng)態(tài)設(shè)置 Ripple 起始圓心的位置和 Ripple 大小。這個(gè)大小應(yīng)基于按鈕的大小,而位置應(yīng)基于按鈕和光標(biāo)的位置。
事件綁定
先來綁定 click 事件:
[...document.querySelectorAll(".ripple")].forEach(btn => {
btn.addEventListener("click", showRipple);
});
然后我們可以使用 event.currentTarget 獲取到當(dāng)前元素:
const btn = event.currentTarget;
獲取到了被點(diǎn)擊的按鈕,現(xiàn)在我們來構(gòu)建一個(gè)子元素,并計(jì)算按鈕的半徑大小:
const circle = document.createElement("span");
const diameter = Math.max(button.clientWidth, button.clientHeight);
const radius = diameter / 2;
現(xiàn)在,我們可以定義我們需要為我們的漣漪其余屬性:left、top、width 和 height。
數(shù)據(jù)計(jì)算
我們知道,top 應(yīng)該等于點(diǎn)擊事件的 (x, y) 減去按鈕的中心點(diǎn)的 (x, y):

例如上面的圖片,圓心中心點(diǎn)應(yīng)該就是 (918 - 323, 392 - 244) 即 (595, 148)。
因此,我們可以得出應(yīng)該這樣設(shè)置這個(gè)圓:
circle.style.width = circle.style.height = `${diameter}px`;
circle.style.left = `${event.clientX - (button.offsetLeft + radius)}px`;
circle.style.top = `${event.clientY - (button.offsetTop + radius)}px`;
circle.classList.add("ripple");
然后現(xiàn)在我們將這個(gè) circle 添加到 btn 即可:
btn.appendChild(circle);
完整的代碼就是:
const showRipple = (event) => {
const btn = event.currentTarget;
const circle = document.createElement("span");
const diameter = Math.max(btn.clientWidth, btn.clientHeight);
const radius = diameter / 2;
circle.style.width = circle.style.height = `${diameter}px`;
circle.style.left = `${event.clientX - (btn.offsetLeft + radius)}px`;
circle.style.top = `${event.clientY - (btn.offsetTop + radius)}px`;
circle.classList.add("ripple");
btn.appendChild(circle);
setTimeout(() => {
btn.removeChild(circle)
}, 1000); /* 記得移除元素 */
}
Show Time!

這就滿足了嗎??? 未嘗也太簡單了吧?
監(jiān)聽頁面元素更新
現(xiàn)在我們需要監(jiān)聽所有元素的更新!自動(dòng)讓系統(tǒng)為所有新增的按鈕添加一樣的動(dòng)畫?。?!
到我們的 MutationObserver 發(fā)揮它的作用啦?。。?/p>
我們先需要定義一個(gè)接受事件并處理數(shù)據(jù)的函數(shù),先暫且命名為 listener:
const listener = (mutationRecord) => {
/**
* @param mutationRecord: Callback of MutationObserve
* => mutations: MutationRecord[]
*/
}
然后定義一個(gè)監(jiān)聽工具并初始化:
const mutationObserver = new MutationObserver(listener);
mutationObserver.observe(document, {subtree: true, childList: true, attributes: true});
一般來說,可能會(huì)有兩種情況:
childList / subtree attributes
屬性變化
如果是元素的屬性變化,那么 mutationRecord.type 會(huì)是 attributes,那么我們直接:
if (mutationRecord.type === "attributes" && mutationRecord.attributeName === "ripple" && !mutationRecord.target.hasAttribute("ripple-init")) {
mutationRecord.target.addEventListener("click", showRipple);
mutationRecord.target.setAttribute("ripple-init", "");
}
元素變化
而如果是生成了元素,那么也很簡單粗暴,直接遍歷 mutationRecord.addedNodes 即可:
if (mutationRecord.addedNodes && mutationRecord.addedNodes.length > 0)
mutationRecord.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE && !node.hasAttribute("ripple-init") && node.hasAttribute("ripple")) {
node.addEventListener("click", showRipple);
node.setAttribute("ripple-init", "");
}
});
讓我們來測試一下效果吧,就用 setTimeout 在 100ms 以后生成一個(gè) .ripple 的按鈕吧:
setTimeout(() => {
document.querySelector("button").setAttribute("ripple", "");
let btn = document.createElement("button");
btn.setAttribute("ripple", "");
btn.innerText = "這是另外一個(gè)簡單的按鈕"
document.body.appendChild(btn);
}, 2000);

總結(jié)思考
看了看 GitHub 的文件,一年前的更新啊……

似乎也沒什么可以改進(jìn)的(誤)
支持更多種類的 Material Button 的 Ripple 效果 將 MutationObserver推廣應(yīng)用在別的地方應(yīng)用這段代碼(當(dāng)時(shí)也是無聊,學(xué)了一下,而我卻也沒有什么網(wǎng)站有很多的按鈕控件,直接改又會(huì)與當(dāng)前的樣式不搭配)
歡迎各位一起加入 掘金翻譯計(jì)劃大家庭,一起助力掘金變得更棒
本文正在參與「掘金 2021 春招闖關(guān)活動(dòng)」, 點(diǎn)擊查看 活動(dòng)詳情

