Varlet組件實(shí)現(xiàn)一個(gè)絲滑的點(diǎn)擊水波效果詳解
正文
讀完本篇,可以了解到如何使用一個(gè)div
創(chuàng)建一個(gè)點(diǎn)擊的水波效果。
Varlet
組件庫(kù)提供了一個(gè)使元素點(diǎn)擊時(shí)生成水波擴(kuò)散效果的指令:
<template> <div v-ripple>點(diǎn)擊</div> </template>
接下來(lái)就從源碼角度看看它是如何實(shí)現(xiàn)的。
首先在指令所綁定的目標(biāo)元素被掛載的時(shí)候會(huì)執(zhí)行如下方法:
function mounted(el: RippleHTMLElement, binding: DirectiveBinding<RippleOptions>) { // 給元素上添加一個(gè)對(duì)象記錄一些數(shù)據(jù) el._ripple = { tasker: null, ...(binding.value ?? {}), touchmoveForbid: binding.value?.touchmoveForbid ?? context.touchmoveForbid, removeRipple: removeRipple.bind(el), } // 給元素綁定了一些事件 el.addEventListener('touchstart', createRipple, { passive: true }) el.addEventListener('touchmove', forbidRippleTask, { passive: true }) el.addEventListener('dragstart', removeRipple, { passive: true }) document.addEventListener('touchend', el._ripple.removeRipple, { passive: true }) document.addEventListener('touchcancel', el._ripple.removeRipple, { passive: true }) }
主要就是綁定了一些事件,處理函數(shù)一共有三個(gè),從函數(shù)名中也可以大致看出其作用。
注意看addEventListener
方法的第三個(gè)參數(shù)中都設(shè)置了passive = true
,這個(gè)選項(xiàng)用來(lái)告訴瀏覽器我們的處理函數(shù)中不會(huì)調(diào)用preventDefault
方法,這么做有什么好處呢?比如touch
事件或scroll
事件的默認(rèn)行為都會(huì)觸發(fā)頁(yè)面的滾動(dòng),如果調(diào)用了preventDefault
方法,那么就會(huì)阻止?jié)L動(dòng),但問(wèn)題是瀏覽器并不知道我們有沒(méi)有在事件處理函數(shù)中調(diào)這個(gè)方法,那么就必須等待函數(shù)執(zhí)行完畢才知道,有時(shí)候函數(shù)的執(zhí)行是比較耗時(shí)的,這樣就會(huì)導(dǎo)致頁(yè)面卡頓,所以如果我們的處理函數(shù)中明確不會(huì)調(diào)用preventDefault
方法,那么就通過(guò)passive
標(biāo)志直接告訴瀏覽器,這樣瀏覽器就不會(huì)等待,直接進(jìn)行滾動(dòng),可以顯著提升頁(yè)面性能和體驗(yàn)。
touchstart 事件處理
createRipple 方法
先看看touchstart
事件的處理方法createRipple
:
function createRipple(this: RippleHTMLElement, event: TouchEvent) { // 首先獲取該元素上存儲(chǔ)的數(shù)據(jù) const _ripple = this._ripple as RippleOptions // 先移除上一個(gè)水波 _ripple.removeRipple() // 如果禁用或者上一個(gè)水波任務(wù)還未執(zhí)行則返回 if (_ripple.disabled || _ripple.tasker) { return } // 水波任務(wù) const task = () => { // ... } // 保存定時(shí)器 _ripple.tasker = window.setTimeout(task, 60) }
當(dāng)我們觸摸點(diǎn)擊一個(gè)元素的時(shí)候,會(huì)先移除該元素的上一個(gè)水波,然后添加一個(gè)新的水波任務(wù),這個(gè)任務(wù)會(huì)在一個(gè)60ms
的定時(shí)器后執(zhí)行,然后把定時(shí)器id
保存起來(lái),為什么不立即執(zhí)行呢,應(yīng)該是為了能夠取消吧,比如想在touchmove
情況下不開(kāi)啟水波效果,那么就可以通過(guò)取消這個(gè)定時(shí)器來(lái)實(shí)現(xiàn),看一下touchmove
事件的處理函數(shù)forbidRippleTask
:
forbidRippleTask 方法
function forbidRippleTask(this: RippleHTMLElement) { const _ripple = this._ripple as RippleOptions // 是否需要在觸摸移動(dòng)時(shí)禁用水波效果 if (!_ripple.touchmoveForbid) { return } // 如果在60ms內(nèi)觸摸移動(dòng)了就會(huì)取消定時(shí)器,自然水波效果就不會(huì)有了 _ripple.tasker && window.clearTimeout(_ripple.tasker) _ripple.tasker = null }
接下來(lái)看看task
方法:
function createRipple(this: RippleHTMLElement, event: TouchEvent) { //... const task = () => { // 定時(shí)器任務(wù)執(zhí)行了則把保存的定時(shí)器id清空 _ripple.tasker = null // 計(jì)算一些數(shù)據(jù) const { x, y, centerX, centerY, size }: RippleStyles = computeRippleStyles(this, event) // 創(chuàng)建一個(gè)div const ripple: RippleHTMLElement = document.createElement('div') // 添加一個(gè)var-ripple類(lèi)名 ripple.classList.add(n()) // 設(shè)置透明度為0,即全透明 ripple.style.opacity = `0` // 設(shè)置位置及縮放 ripple.style.transform = `translate(${x}px, ${y}px) scale3d(.3, .3, .3)` // 設(shè)置大小 ripple.style.width = `${size}px` ripple.style.height = `${size}px` // 設(shè)置顏色 _ripple.color && (ripple.style.backgroundColor = _ripple.color) // 記錄創(chuàng)建時(shí)間 ripple.dataset.createdAt = String(performance.now()) // 設(shè)置被點(diǎn)擊元素的樣式 setStyles(this) // 將水波元素添加到被點(diǎn)擊元素內(nèi) this.appendChild(ripple) // 20ms后修改水波元素的樣式,達(dá)到水波的擴(kuò)散動(dòng)畫(huà)效果 window.setTimeout(() => { ripple.style.transform = `translate(${centerX}px, ${centerY}px) scale3d(1, 1, 1)` ripple.style.opacity = `.25` }, 20) } //... }
可以看到所謂水波就是一個(gè)div
,總體的流程為先創(chuàng)建一個(gè)div
元素,然后設(shè)置它的透明度為0
、初始位置、縮放、大小、背景顏色,然后添加為被點(diǎn)擊元素的子元素,最后在20ms
以后修改div
的位置、縮放、透明度,只要設(shè)置了它的transation
過(guò)渡屬性即可實(shí)現(xiàn)過(guò)渡效果,也就是水波擴(kuò)散的效果,樣式是通過(guò)類(lèi)名var-ripple
設(shè)置的:
:root { --ripple-cubic-bezier: cubic-bezier(0.68, 0.01, 0.62, 0.6); --ripple-color: currentColor; } .var-ripple { position: absolute;// 設(shè)置為絕對(duì)定位 transition: transform 0.2s var(--ripple-cubic-bezier), opacity 0.14s linear;// 設(shè)置過(guò)渡效果 top: 0; left: 0; border-radius: 50%;// 設(shè)置為圓形 opacity: 0; will-change: transform, opacity; pointer-events: none;// 禁止響應(yīng)鼠標(biāo)事件 z-index: 100; background-color: var(--ripple-color);// 背景顏色 }
可以看到水波元素為絕對(duì)定位,另外位置的過(guò)渡時(shí)間為200ms
,透明度的過(guò)渡時(shí)間為140ms
。
接下來(lái)看看其中調(diào)用的幾個(gè)函數(shù)。
調(diào)用computeRippleStyles方法計(jì)算
首先是調(diào)用computeRippleStyles
方法計(jì)算一些基本數(shù)據(jù):
function computeRippleStyles(element: RippleHTMLElement, event: TouchEvent): RippleStyles { // 被點(diǎn)擊元素距離屏幕頂部和左側(cè)的距離 const { top, left }: DOMRect = element.getBoundingClientRect() // 被點(diǎn)擊元素的寬高 const { clientWidth, clientHeight } = element // 計(jì)算水波圓的半徑 const radius: number = Math.sqrt(clientWidth ** 2 + clientHeight ** 2) / 2 // 直徑 const size: number = radius * 2 // ... }
水波的直徑是根據(jù)勾股定理計(jì)算的:
function computeRippleStyles(element: RippleHTMLElement, event: TouchEvent): RippleStyles { // ... // 手指點(diǎn)擊的位置相對(duì)于被點(diǎn)擊元素的坐標(biāo) const localX: number = event.touches[0].clientX - left const localY: number = event.touches[0].clientY - top // 水波元素初始位置 const x: number = localX - radius const y: number = localY - radius // 水波元素最終位置 const centerX: number = (clientWidth - radius * 2) / 2 const centerY: number = (clientHeight - radius * 2) / 2 return { x, y, centerX, centerY, size } }
size
為水波圓的直徑;
手指點(diǎn)擊的位置是水波圓初始的中心點(diǎn),然后計(jì)算其左上角坐標(biāo)x、y
為水波元素的初始位置;
水波圓的最終中心點(diǎn)其實(shí)就是被點(diǎn)擊元素的中心點(diǎn),換算成左上角坐標(biāo)centerX、centerY
即為水波元素的最終位置。
因?yàn)樗ㄔ貫楸稽c(diǎn)擊元素的子元素,所以這些坐標(biāo)都是相對(duì)于被點(diǎn)擊元素的左上角坐標(biāo)計(jì)算的:
從綠色的圓過(guò)渡成紅色的圓,透明度、大小、位置的變化就是水波的擴(kuò)散效果。
調(diào)用setStyles方法
將水波元素添加到被點(diǎn)擊元素內(nèi)前還調(diào)用了setStyles
方法:
function setStyles(element: RippleHTMLElement) { const { zIndex, position } = window.getComputedStyle(element) element.style.overflow = 'hidden' element.style.overflowX = 'hidden' element.style.overflowY = 'hidden' position === 'static' && (element.style.position = 'relative') zIndex === 'auto' && (element.style.zIndex = '1') }
這個(gè)函數(shù)做的事情主要是檢查和設(shè)置被點(diǎn)擊元素的一些樣式,首先溢出需要設(shè)置為隱藏,否則水波圓的擴(kuò)散就會(huì)溢出元素完整顯示出來(lái),這顯然不好看,然后前面提到過(guò)水波元素為絕對(duì)定位,所以被點(diǎn)擊元素的定位不能是靜態(tài)定位,最后的層級(jí)設(shè)置筆者暫時(shí)沒(méi)有想出來(lái)是為了解決什么問(wèn)題。
removeRipple方法
到這里,當(dāng)我們手觸摸元素時(shí),水波效果就創(chuàng)建完成了,接下來(lái)是移除操作,看一下removeRipple
方法:
const ANIMATION_DURATION = 250 function removeRipple(this: RippleHTMLElement) { const _ripple = this._ripple as RippleOptions const task = () => { // 獲取水波元素 const ripples: NodeListOf<RippleHTMLElement> = this.querySelectorAll(`.${n()}`) if (!ripples.length) { return } // 最后一個(gè)水波 const lastRipple: RippleHTMLElement = ripples[ripples.length - 1] // 計(jì)算延遲時(shí)間 const delay: number = ANIMATION_DURATION - performance.now() + Number(lastRipple.dataset.createdAt) // 延遲后將水波的透明度設(shè)置為0 setTimeout(() => { lastRipple.style.opacity = `0` // 再次延遲后移除水波元素 setTimeout(() => lastRipple.parentNode?.removeChild(lastRipple), ANIMATION_DURATION) }, delay) } // 創(chuàng)建任務(wù)的定時(shí)器id存在則等待60ms _ripple.tasker ? setTimeout(task, 60) : task() }
先回顧一下創(chuàng)建水波的各個(gè)階段的耗時(shí),當(dāng)我們第一次點(diǎn)擊元素時(shí),等待60ms
后會(huì)創(chuàng)建水波元素,然后再等待20ms
后會(huì)開(kāi)始進(jìn)行水波的擴(kuò)散效果,動(dòng)畫(huà)耗時(shí)200ms
結(jié)束,如果我們?cè)?code>60ms內(nèi)進(jìn)行第二次點(diǎn)擊不會(huì)創(chuàng)建第二個(gè)水波,因?yàn)榍耙粋€(gè)水波任務(wù)還未執(zhí)行,如果是在60ms
后第二次點(diǎn)擊,會(huì)先調(diào)用removeRipplie
移除上一個(gè)水波,然后重復(fù)第一個(gè)水波的創(chuàng)建流程:
每次執(zhí)行removeRipple
方法只需要移除當(dāng)前最后一個(gè)水波即可,之前的水波會(huì)由之前的task
移除。
接下來(lái)詳細(xì)看看整個(gè)過(guò)程。
當(dāng)手指第一次觸摸點(diǎn)擊元素時(shí)會(huì)執(zhí)行createRipple
方法,方法內(nèi)會(huì)先執(zhí)行removeRipple
方法,此時(shí)_ripple.tasker
不存在,會(huì)立即執(zhí)行removeRipple
的task
方法,但是目前并沒(méi)有水波元素,所以這個(gè)函數(shù)會(huì)直接返回,removeRipple
方法執(zhí)行完畢。
接下來(lái)會(huì)創(chuàng)建一個(gè)60ms
的定時(shí)器,等待執(zhí)行createRipple
的task
,如果我們?cè)?code>60ms內(nèi)就松開(kāi)了手指,那么又會(huì)執(zhí)行removeRipple
方法,此時(shí)_ripple.tasker
存在,所以removeRipple
的task
方法也會(huì)等待60ms
再執(zhí)行;如果我們是在60ms
后才松開(kāi)手指,那么_ripple.tasker
不存在,會(huì)立即執(zhí)行removeRipple
的task
方法,該方法內(nèi)會(huì)獲取最后一個(gè)水波元素,也就是剛剛創(chuàng)建的水波元素,然后計(jì)算delay
:
delay = ANIMATION_DURATION - (performance.now() - Number(lastRipple.dataset.createdAt))
performance.now() - Number(lastRipple.dataset.createdAt)
代表此刻到創(chuàng)建水波時(shí)過(guò)去的時(shí)間,ANIMATION_DURATION
減去它即表示250ms
還剩下的時(shí)間,因?yàn)榍懊嫣岬搅怂◤膭?chuàng)建到擴(kuò)散完成整個(gè)過(guò)程大概耗時(shí)20ms + 200ms = 220ms
,所以延遲dealy
時(shí)間,也就是等待水波動(dòng)畫(huà)完成后再讓水波消失,避免水波還未擴(kuò)散完成就消失的情況,修改水波的透明度為0
,透明度動(dòng)畫(huà)耗時(shí)140ms
,所以再等待250ms
將水波元素移除。
如果在60ms
內(nèi)松開(kāi)手指又立即再次觸摸元素,那么又會(huì)執(zhí)行createRipple
方法,同樣又會(huì)先執(zhí)行removeRipple
方法,此時(shí)前一個(gè)創(chuàng)建水波的task
任務(wù)還未執(zhí)行,_ripple.tasker
存在,所以removeRipple
的task
方法會(huì)等待60ms
再執(zhí)行,這個(gè)task
任務(wù)其實(shí)和松開(kāi)手指時(shí)觸發(fā)的task
任務(wù)重復(fù)了,相當(dāng)于兩個(gè)task
移除同一個(gè)水波元素,不過(guò)問(wèn)題也不大。
因?yàn)樯弦粋€(gè)水波的task
還未執(zhí)行,所以createRipple
會(huì)直接返回。
如果在60ms
后再次觸摸元素,執(zhí)行removeRipple
時(shí)_ripple.tasker
不存在,會(huì)立即執(zhí)行task
方法,同樣,這個(gè)task
任務(wù)也會(huì)和松開(kāi)手指觸發(fā)的task
任務(wù)重復(fù)。
此時(shí)_ripple.tasker
不存在,所以創(chuàng)建第二個(gè)水波的任務(wù)會(huì)被添加到定時(shí)器里,當(dāng)?shù)诙嗡砷_(kāi)手指時(shí),執(zhí)行removeRiplle
會(huì)刪除第二個(gè)水波。
更多次重復(fù)觸摸元素時(shí)以此類(lèi)推,會(huì)不斷創(chuàng)建水波,水波動(dòng)畫(huà)結(jié)束后也會(huì)不斷被刪除。
在目標(biāo)元素被卸載時(shí)會(huì)執(zhí)行unmounted
方法:
function unmounted(el: RippleHTMLElement) { el.removeEventListener('touchstart', createRipple) el.removeEventListener('touchmove', forbidRippleTask) el.removeEventListener('dragstart', removeRipple) document.removeEventListener('touchend', el._ripple!.removeRipple) document.removeEventListener('touchcancel', el._ripple!.removeRipple) }
主要是移除綁定的事件。
到這里,水波效果的創(chuàng)建和移除就都介紹完了,可以看到這種實(shí)現(xiàn)方式對(duì)目標(biāo)元素還是有一定要求的,如果目標(biāo)元素的樣式布局需要設(shè)置position
、overflow
、z-index
屬性為不符合要求的值,那么直接修改可能就會(huì)導(dǎo)致樣式出現(xiàn)問(wèn)題,并且卸載時(shí)也沒(méi)有進(jìn)行恢復(fù),這是不是也算是一個(gè)小bug
。
以上就是Varlet組件實(shí)現(xiàn)一個(gè)絲滑的點(diǎn)擊水波效果詳解的詳細(xì)內(nèi)容,更多關(guān)于Varlet組件點(diǎn)擊水波的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Javascript 獲取字符串字節(jié)數(shù)的多種方法
Javascript 字符串字節(jié)數(shù)獲取功能多種方法2009-06-06詳解微信小程序「渲染層網(wǎng)絡(luò)層錯(cuò)誤」的解決方法
這篇文章主要介紹了詳解微信小程序「渲染層網(wǎng)絡(luò)層錯(cuò)誤」的解決方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01javascript基于DOM實(shí)現(xiàn)省市級(jí)聯(lián)下拉框的方法
這篇文章主要介紹了javascript基于DOM實(shí)現(xiàn)省市級(jí)聯(lián)下拉框的方法,可實(shí)現(xiàn)選擇省份后出現(xiàn)對(duì)應(yīng)城市下拉框選項(xiàng)的功能,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-05-05JavaScript ES6中類(lèi)與模塊化管理超詳細(xì)講解
JavaScript中的模塊化是指將每個(gè)js文件會(huì)被認(rèn)為單獨(dú)一個(gè)的模塊。模塊之間是互相不可見(jiàn)的。如果一個(gè)模塊需要使用另一個(gè)模塊,那么需要通過(guò)指定語(yǔ)法來(lái)引入要使用的模塊,而且只能使用引入模塊所暴露的內(nèi)容2023-01-01js使用對(duì)象直接量創(chuàng)建對(duì)象的代碼
js使用對(duì)象直接量創(chuàng)建對(duì)象的代碼...2007-09-09js Object2String方便查看js對(duì)象內(nèi)容
這篇文章主要介紹了將JS的任意對(duì)象輸出為json格式字符串的方法,需要的朋友可以參考下2014-11-11JavaScript與jQuery實(shí)現(xiàn)的閃爍輸入效果
這篇文章主要介紹了JavaScript與jQuery實(shí)現(xiàn)的閃爍輸入效果,結(jié)合實(shí)例形式分別分析了JavaScript與jQuery實(shí)現(xiàn)閃爍輸入效果的方法與具體使用技巧,需要的朋友可以參考下2016-02-02