欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Varlet組件實(shí)現(xiàn)一個(gè)絲滑的點(diǎn)擊水波效果詳解

 更新時(shí)間:2022年10月20日 11:25:35   作者:街角小林  
這篇文章主要為大家介紹了Varlet組件實(shí)現(xiàn)一個(gè)絲滑的點(diǎn)擊水波效果示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jì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í)行removeRippletask方法,但是目前并沒有水波元素,所以這個(gè)函數(shù)會直接返回,removeRipple方法執(zhí)行完畢。

接下來會創(chuàng)建一個(gè)60ms的定時(shí)器,等待執(zhí)行createRippletask,如果我們在60ms內(nèi)就松開了手指,那么又會執(zhí)行removeRipple方法,此時(shí)_ripple.tasker存在,所以removeRippletask方法也會等待60ms再執(zhí)行;如果我們是在60ms后才松開手指,那么_ripple.tasker不存在,會立即執(zhí)行removeRippletask方法,該方法內(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存在,所以removeRippletask方法會等待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)文章

最新評論