Varlet組件實(shí)現(xiàn)一個(gè)絲滑的點(diǎn)擊水波效果詳解
正文
讀完本篇,可以了解到如何使用一個(gè)div
創(chuàng)建一個(gè)點(diǎn)擊的水波效果。
Varlet
組件庫提供了一個(gè)使元素點(diǎn)擊時(shí)生成水波擴(kuò)散效果的指令:
<template> <div v-ripple>點(diǎn)擊</div> </template>
接下來就從源碼角度看看它是如何實(shí)現(xiàn)的。
首先在指令所綁定的目標(biāo)元素被掛載的時(shí)候會執(zhí)行如下方法:
function mounted(el: RippleHTMLElement, binding: DirectiveBinding<RippleOptions>) { // 給元素上添加一個(gè)對象記錄一些數(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)用來告訴瀏覽器我們的處理函數(shù)中不會調(diào)用preventDefault
方法,這么做有什么好處呢?比如touch
事件或scroll
事件的默認(rèn)行為都會觸發(fā)頁面的滾動(dòng),如果調(diào)用了preventDefault
方法,那么就會阻止?jié)L動(dòng),但問題是瀏覽器并不知道我們有沒有在事件處理函數(shù)中調(diào)這個(gè)方法,那么就必須等待函數(shù)執(zhí)行完畢才知道,有時(shí)候函數(shù)的執(zhí)行是比較耗時(shí)的,這樣就會導(dǎo)致頁面卡頓,所以如果我們的處理函數(shù)中明確不會調(diào)用preventDefault
方法,那么就通過passive
標(biāo)志直接告訴瀏覽器,這樣瀏覽器就不會等待,直接進(jìn)行滾動(dòng),可以顯著提升頁面性能和體驗(yàn)。
touchstart 事件處理
createRipple 方法
先看看touchstart
事件的處理方法createRipple
:
function createRipple(this: RippleHTMLElement, event: TouchEvent) { // 首先獲取該元素上存儲的數(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í)候,會先移除該元素的上一個(gè)水波,然后添加一個(gè)新的水波任務(wù),這個(gè)任務(wù)會在一個(gè)60ms
的定時(shí)器后執(zhí)行,然后把定時(shí)器id
保存起來,為什么不立即執(zhí)行呢,應(yīng)該是為了能夠取消吧,比如想在touchmove
情況下不開啟水波效果,那么就可以通過取消這個(gè)定時(shí)器來實(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)了就會取消定時(shí)器,自然水波效果就不會有了 _ripple.tasker && window.clearTimeout(_ripple.tasker) _ripple.tasker = null }
接下來看看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類名 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)畫效果 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
過渡屬性即可實(shí)現(xiàn)過渡效果,也就是水波擴(kuò)散的效果,樣式是通過類名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è)置為絕對定位 transition: transform 0.2s var(--ripple-cubic-bezier), opacity 0.14s linear;// 設(shè)置過渡效果 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);// 背景顏色 }
可以看到水波元素為絕對定位,另外位置的過渡時(shí)間為200ms
,透明度的過渡時(shí)間為140ms
。
接下來看看其中調(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)擊的位置相對于被點(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)都是相對于被點(diǎn)擊元素的左上角坐標(biāo)計(jì)算的:
從綠色的圓過渡成紅色的圓,透明度、大小、位置的變化就是水波的擴(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ò)散就會溢出元素完整顯示出來,這顯然不好看,然后前面提到過水波元素為絕對定位,所以被點(diǎn)擊元素的定位不能是靜態(tài)定位,最后的層級設(shè)置筆者暫時(shí)沒有想出來是為了解決什么問題。
removeRipple方法
到這里,當(dāng)我們手觸摸元素時(shí),水波效果就創(chuàng)建完成了,接下來是移除操作,看一下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
后會創(chuàng)建水波元素,然后再等待20ms
后會開始進(jìn)行水波的擴(kuò)散效果,動(dòng)畫耗時(shí)200ms
結(jié)束,如果我們在60ms
內(nèi)進(jìn)行第二次點(diǎn)擊不會創(chuàng)建第二個(gè)水波,因?yàn)榍耙粋€(gè)水波任務(wù)還未執(zhí)行,如果是在60ms
后第二次點(diǎn)擊,會先調(diào)用removeRipplie
移除上一個(gè)水波,然后重復(fù)第一個(gè)水波的創(chuàng)建流程:
每次執(zhí)行removeRipple
方法只需要移除當(dāng)前最后一個(gè)水波即可,之前的水波會由之前的task
移除。
接下來詳細(xì)看看整個(gè)過程。
當(dāng)手指第一次觸摸點(diǎn)擊元素時(shí)會執(zhí)行createRipple
方法,方法內(nèi)會先執(zhí)行removeRipple
方法,此時(shí)_ripple.tasker
不存在,會立即執(zhí)行removeRipple
的task
方法,但是目前并沒有水波元素,所以這個(gè)函數(shù)會直接返回,removeRipple
方法執(zhí)行完畢。
接下來會創(chuàng)建一個(gè)60ms
的定時(shí)器,等待執(zhí)行createRipple
的task
,如果我們在60ms
內(nèi)就松開了手指,那么又會執(zhí)行removeRipple
方法,此時(shí)_ripple.tasker
存在,所以removeRipple
的task
方法也會等待60ms
再執(zhí)行;如果我們是在60ms
后才松開手指,那么_ripple.tasker
不存在,會立即執(zhí)行removeRipple
的task
方法,該方法內(nèi)會獲取最后一個(gè)水波元素,也就是剛剛創(chuàng)建的水波元素,然后計(jì)算delay
:
delay = ANIMATION_DURATION - (performance.now() - Number(lastRipple.dataset.createdAt))
performance.now() - Number(lastRipple.dataset.createdAt)
代表此刻到創(chuàng)建水波時(shí)過去的時(shí)間,ANIMATION_DURATION
減去它即表示250ms
還剩下的時(shí)間,因?yàn)榍懊嫣岬搅怂◤膭?chuàng)建到擴(kuò)散完成整個(gè)過程大概耗時(shí)20ms + 200ms = 220ms
,所以延遲dealy
時(shí)間,也就是等待水波動(dòng)畫完成后再讓水波消失,避免水波還未擴(kuò)散完成就消失的情況,修改水波的透明度為0
,透明度動(dòng)畫耗時(shí)140ms
,所以再等待250ms
將水波元素移除。
如果在60ms
內(nèi)松開手指又立即再次觸摸元素,那么又會執(zhí)行createRipple
方法,同樣又會先執(zhí)行removeRipple
方法,此時(shí)前一個(gè)創(chuàng)建水波的task
任務(wù)還未執(zhí)行,_ripple.tasker
存在,所以removeRipple
的task
方法會等待60ms
再執(zhí)行,這個(gè)task
任務(wù)其實(shí)和松開手指時(shí)觸發(fā)的task
任務(wù)重復(fù)了,相當(dāng)于兩個(gè)task
移除同一個(gè)水波元素,不過問題也不大。
因?yàn)樯弦粋€(gè)水波的task
還未執(zhí)行,所以createRipple
會直接返回。
如果在60ms
后再次觸摸元素,執(zhí)行removeRipple
時(shí)_ripple.tasker
不存在,會立即執(zhí)行task
方法,同樣,這個(gè)task
任務(wù)也會和松開手指觸發(fā)的task
任務(wù)重復(fù)。
此時(shí)_ripple.tasker
不存在,所以創(chuàng)建第二個(gè)水波的任務(wù)會被添加到定時(shí)器里,當(dāng)?shù)诙嗡砷_手指時(shí),執(zhí)行removeRiplle
會刪除第二個(gè)水波。
更多次重復(fù)觸摸元素時(shí)以此類推,會不斷創(chuàng)建水波,水波動(dòng)畫結(jié)束后也會不斷被刪除。
在目標(biāo)元素被卸載時(shí)會執(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)方式對目標(biāo)元素還是有一定要求的,如果目標(biāo)元素的樣式布局需要設(shè)置position
、overflow
、z-index
屬性為不符合要求的值,那么直接修改可能就會導(dǎo)致樣式出現(xiàn)問題,并且卸載時(shí)也沒有進(jìn)行恢復(fù),這是不是也算是一個(gè)小bug
。
以上就是Varlet組件實(shí)現(xiàn)一個(gè)絲滑的點(diǎn)擊水波效果詳解的詳細(xì)內(nèi)容,更多關(guān)于Varlet組件點(diǎn)擊水波的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Javascript 獲取字符串字節(jié)數(shù)的多種方法
Javascript 字符串字節(jié)數(shù)獲取功能多種方法2009-06-06詳解微信小程序「渲染層網(wǎng)絡(luò)層錯(cuò)誤」的解決方法
這篇文章主要介紹了詳解微信小程序「渲染層網(wǎng)絡(luò)層錯(cuò)誤」的解決方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01javascript基于DOM實(shí)現(xiàn)省市級聯(lián)下拉框的方法
這篇文章主要介紹了javascript基于DOM實(shí)現(xiàn)省市級聯(lián)下拉框的方法,可實(shí)現(xiàn)選擇省份后出現(xiàn)對應(yīng)城市下拉框選項(xiàng)的功能,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-05-05JavaScript ES6中類與模塊化管理超詳細(xì)講解
JavaScript中的模塊化是指將每個(gè)js文件會被認(rèn)為單獨(dú)一個(gè)的模塊。模塊之間是互相不可見的。如果一個(gè)模塊需要使用另一個(gè)模塊,那么需要通過指定語法來引入要使用的模塊,而且只能使用引入模塊所暴露的內(nèi)容2023-01-01js Object2String方便查看js對象內(nèi)容
這篇文章主要介紹了將JS的任意對象輸出為json格式字符串的方法,需要的朋友可以參考下2014-11-11JavaScript與jQuery實(shí)現(xiàn)的閃爍輸入效果
這篇文章主要介紹了JavaScript與jQuery實(shí)現(xiàn)的閃爍輸入效果,結(jié)合實(shí)例形式分別分析了JavaScript與jQuery實(shí)現(xiàn)閃爍輸入效果的方法與具體使用技巧,需要的朋友可以參考下2016-02-02