自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

原生JS手寫(xiě)絲滑流暢的元素拖拽效果

開(kāi)發(fā) 前端
我們需要知道鼠標(biāo)的三個(gè)事件,分別是 mousedown,mousemove,mouseup ,當(dāng)點(diǎn)擊按下的時(shí)候,克隆一個(gè)絕對(duì)定位的元素,并標(biāo)識(shí)下"拖拽中"的狀態(tài),接著在 mousemove 中就可以判斷應(yīng)該執(zhí)行的具體方法,從而讓元素隨著鼠標(biāo)移動(dòng)起來(lái)。

前言

提到元素拖拽,通常都會(huì)先想到用 HTML5 的拖拽放置 (Drag 和 Drop) 來(lái)實(shí)現(xiàn),它提供了一套完整的事件機(jī)制,看起來(lái)似乎是首選的解決方案,但實(shí)際卻不是那么美好,主要是它的樣式太過(guò)簡(jiǎn)陋,無(wú)法實(shí)現(xiàn)更高級(jí)的用戶(hù)體驗(yàn):

這是瀏覽器默認(rèn)的拖拽效果,點(diǎn)住拖拽任意圖片或文字都會(huì)產(chǎn)生。

筆者因?yàn)橹坝袀€(gè)小項(xiàng)目需要經(jīng)常參考稿定設(shè)計(jì),一直有留意其元素拖拽的效果(如下圖),所以接下來(lái)我將以這種效果為藍(lán)本,使用原生 JS 實(shí)現(xiàn)一個(gè)富有動(dòng)感的 自定義拖拽 效果,話(huà)不多說(shuō)直接開(kāi)摸。

實(shí)現(xiàn)原理

首先說(shuō)下思路,我們需要知道鼠標(biāo)的三個(gè)事件,分別是 mousedown,mousemove,mouseup ,當(dāng)點(diǎn)擊按下的時(shí)候,克隆一個(gè)絕對(duì)定位的元素,并標(biāo)識(shí)下"拖拽中"的狀態(tài),接著在 mousemove 中就可以判斷應(yīng)該執(zhí)行的具體方法,從而讓元素隨著鼠標(biāo)移動(dòng)起來(lái)。

在監(jiān)聽(tīng)事件的 event 對(duì)象中,有幾個(gè)參數(shù)是比較重要的:clientX,clientY 標(biāo)識(shí)的鼠標(biāo)當(dāng)前橫坐標(biāo)和縱坐標(biāo),offsetX 和 offsetY 表示相對(duì)偏移量,可以在 mousedown 鼠標(biāo)按下時(shí)記錄初始坐標(biāo),在 mouseup 鼠標(biāo)抬起時(shí)判斷是否在目標(biāo)區(qū)域中,如果是則用鼠標(biāo)獲取到的當(dāng)前的偏移量 - 初始坐標(biāo)得到元素實(shí)際在目標(biāo)區(qū)域中的位置。

為了閱讀體驗(yàn),以下所有代碼均有部分省略,文末可查看完整源碼地址,代碼量并不多。

基礎(chǔ)界面

先簡(jiǎn)單實(shí)現(xiàn)一個(gè)兩欄布局界面,并應(yīng)用上一些 CSS 效果:

<div id="app">
<div class="slide">
<div id="list">
<img class="item" src="......." />
<img .........
</div>
</div>
<div class="content"></div>
</div>
#app {
width: 100vw;
height: 100vh;
display: flex;
}
.active {
cursor: grabbing;
}

.slide {
width: 260px;
height: 100%;
overflow: scroll;
border-right: 1px solid rgba(0,0,0,.15);
#list {
user-select: none;
.item {
background: rgba(0,0,0,.15);
width: 120px;
display: inline-block;
break-inside: avoid;
margin-bottom: 4px;
}
.item:hover {
cursor: grab;
filter: brightness(90%);
}
.item:active {
cursor: grabbing;
}
}
.grid {
column-count: 2;
column-gap: 0px;
}
}
.slide::-webkit-scrollbar {
display: none; /* Chrome Safari */
}

#content {
position: relative;
flex: 1;
height: 100%;
margin-left: 45px;
background: rgba(0,0,0,.07);
.item {
position: absolute;
transform-origin: top left;
}
}

利用濾鏡 filter: brightness(90%); 調(diào)節(jié)明亮度可以快速實(shí)現(xiàn)一個(gè)鼠標(biāo)覆蓋的動(dòng)態(tài)效果,無(wú)需額外制作遮罩:

使用偽類(lèi)激活 cursor 的 grab 和 grabbing 可以設(shè)置抓取動(dòng)作的圖標(biāo):

實(shí)現(xiàn)元素抓取

利用事件委托機(jī)制為選擇列表添加 mousedown 事件監(jiān)聽(tīng),實(shí)現(xiàn)抓取的原理是在鼠標(biāo)按下時(shí)克隆按下的元素,并把克隆出來(lái)的元素設(shè)置成絕對(duì)定位,讓它"浮"起來(lái):

let dragging = false
let cloneEl = null // 克隆元素
let initial = {} // 初始化數(shù)據(jù)記錄
......
// 選中了元素
cloneEl = e.target.cloneNode(true) // 克隆元素
cloneEl.classList.add('flutter') // 使其浮動(dòng)
e.target.parentElement.appendChild(cloneEl) // 加入到列表中
dragging = true // 標(biāo)記拖動(dòng)開(kāi)始

// TODO:
........
.flutter {
position: absolute;
z-index: 9999;
pointer-events: none;
}

將鼠標(biāo)的坐標(biāo)設(shè)置為克隆元素的絕對(duì)定位值(left、top),就會(huì)像下圖所示這樣,此時(shí)減去 offset 偏移量,就能讓克隆元素覆蓋在本體上面。

初始化的值需要記錄起來(lái)方便后續(xù)計(jì)算,同時(shí)我們用 dragging 變量標(biāo)記了狀態(tài)(拖動(dòng)中),接下來(lái)配合移動(dòng)鼠標(biāo)的監(jiān)聽(tīng)事件就能將元素“抓”起來(lái)了:

// 鼠標(biāo)移動(dòng)
window.addEventListener("mousemove", (e) => {
if (dragging && cloneEl) {
// TODO:
// x 軸(left)計(jì)算方法:e.clientX - initial.offsetX
// y 軸(top)計(jì)算方法:e.clientY - initial.offsetY
}
})

上面只是實(shí)現(xiàn)了元素的拖動(dòng),但是"克隆"的效果實(shí)在太明顯了,為了讓元素看起來(lái)更像是拖出來(lái)的而不是復(fù)制出來(lái)的,我們還要讓本體隱藏,同時(shí)DOM結(jié)構(gòu)不能丟失,這時(shí)只需在按下拖動(dòng)時(shí)給本體元素設(shè)置個(gè) opacity: 0,結(jié)束時(shí)再改回透明度1就能搞定。

雖然到這功能就算實(shí)現(xiàn)了,但實(shí)際效果還是有點(diǎn)僵硬,參考稿定設(shè)計(jì)中的元素放開(kāi)時(shí)會(huì)固定回到一個(gè)位置,然后再收回去,這個(gè)過(guò)渡又有點(diǎn)鬼畜,不夠流暢。其實(shí)只需讓元素回退過(guò)程有一個(gè)自然地動(dòng)畫(huà)就行,transition 就能實(shí)現(xiàn):

.is_return {
transition: all 0.3s;
}
// 鼠標(biāo)抬起
window.addEventListener("mouseup", (e) => {
dragging = false
if (cloneEl) {
cloneEl.classList.add('is_return') // 加上過(guò)渡動(dòng)畫(huà)
changeStyle(......) // 設(shè)置回元素的初始位置
setTimeout(() {
cloneEl.remove() // 移除元素
}, 300)
}
})

最終我在動(dòng)作結(jié)束時(shí)給克隆元素添加了過(guò)渡屬性,然后直接設(shè)置回初始坐標(biāo)讓克隆元素回到它的出生地點(diǎn),用定時(shí)器在過(guò)渡動(dòng)畫(huà)持續(xù)的相同時(shí)間后移除克隆元素,這樣就有了一個(gè)平滑穩(wěn)定的回退動(dòng)畫(huà)。

性能優(yōu)化

由于在改變?cè)貭顟B(tài)的過(guò)程中需要頻繁進(jìn)行多個(gè) CSS 操作,為降低回流重繪的成本,最好將多個(gè)操作合并起來(lái)處理,這里利用了 cssText 來(lái)實(shí)現(xiàn):

// 改變漂浮元素:x、y、縮放倍率
function moveFlutter(x, y, d = 0){
const scale = d ? initial.width + d < initial.fakeSize ? `transform: scale(${(initial.width + d) / initial.width})` : null : null
const options = [`left: ${x}px`, `top: ${y}px`]
scale && options.push(scale)
// 將CSS處理成數(shù)組,然后丟進(jìn)DOM操作方法中一次執(zhí)行
changeStyle(options)
}
// 合并多個(gè)操作
function changeStyle(arr){
const original = cloneEl.style.cssText.split(';')
original.pop()
cloneEl.style.cssText = original.concat(arr).join(';') + ';'
}

實(shí)現(xiàn)拖拽放大

放大我們可以使用 transform: scale 來(lái)實(shí)現(xiàn),只需要將拖動(dòng)位置之間的距離當(dāng)做變化系數(shù)(假設(shè)為d),那么scale變化數(shù)值即為(元素寬度 + d)/元素寬度,而放大的最終倍數(shù)必定為 圖片實(shí)際寬度/元素的寬度,只要判斷不超過(guò)這個(gè)邊界就可以。(這個(gè)圖片實(shí)際寬高在真實(shí)業(yè)務(wù)場(chǎng)景中建議在上傳資源時(shí)就記錄在數(shù)據(jù)庫(kù),這里我是模擬的隨機(jī)一個(gè)原圖尺寸)。

兩點(diǎn)間距離計(jì)算公式為:

代碼實(shí)現(xiàn):

// 計(jì)算兩點(diǎn)之間距離
function distance({ clientX, clientY }){
const { clientX: x, clientY: y } = initial // 獲取初始的坐標(biāo)
const b = clientX - x;
const a = clientY - y;
return Math.sqrt(Math.pow(b, 2) + Math.pow(a, 2))
}

window.addEventListener("mousemove", (e) => {
if (dragging && cloneEl) {
const d = distance(e) // 計(jì)算距離
moveFlutter(e.clientX - initial.offsetX, e.clientY - initial.offsetY, d)
}
})
function moveFlutter(x, y, d = 0){
let scale = ''
// 如果距離大于0,且寬度+距離小于實(shí)際寬度
if( d && initial.width + d <= initial.fakeSize ) {
scale = `transform: scale(${(initial.width + d) / initial.width})`
}
// TODO ... changeStyle ...
}

效果演示:

注意元素都要設(shè)置 transform-origin: top left; 改變縮放原點(diǎn)到左上角,否則默認(rèn)(中心為原點(diǎn))的轉(zhuǎn)換會(huì)發(fā)生比較明顯的偏移。

實(shí)現(xiàn)放置

其實(shí)拖拽放置有點(diǎn)像是"復(fù)制"與"粘貼",前面我們實(shí)現(xiàn)了復(fù)制,放置主要就是將元素粘貼到畫(huà)布當(dāng)中,流程步驟如下:

  • 如果鼠標(biāo)在目標(biāo)區(qū)域,拷貝元素到畫(huà)布中,如果不在畫(huà)布中,執(zhí)行倒退動(dòng)畫(huà)
  • 刪除元素
// 完成處理
function done(x, y) {
if (!cloneEl) { return }
const newEl = cloneEl.cloneNode(true)
newEl.classList.remove('flutter')
newEl.src = cloneEl.getAttribute('raw') // 設(shè)置原圖地址
newEl.style.cssText = `left: ${x - initial.offsetX}px; top: ${y - initial.offsetY}px;`
document.getElementById('content').appendChild(newEl)
// TODO:
}

判斷是否在畫(huà)布內(nèi)抬起很簡(jiǎn)單,往畫(huà)布上綁定mouseup監(jiān)聽(tīng)事件即可,克隆的新元素必須刪除無(wú)用的屬性和class,此時(shí)設(shè)置元素的left、top即可將元素放置進(jìn)畫(huà)布中,關(guān)鍵點(diǎn)在于畫(huà)布內(nèi)的target有可能是錯(cuò)的,因?yàn)槿绻髽?biāo)抬起的區(qū)域已經(jīng)放置了元素,那么相對(duì)偏移量就得我們自己計(jì)算了,使用getBoundingClientRect方法獲取畫(huà)布本身相對(duì)于視窗的偏移,鼠標(biāo)坐標(biāo)減去畫(huà)布本身的偏移就是元素在畫(huà)布中的位置了。

document.getElementById('content').addEventListener("mouseup", (e) => {
if (e.target.id !== 'content') {
const lostX = e.x - document.getElementById('content').getBoundingClientRect().left
const lostY = e.y - document.getElementById('content').getBoundingClientRect().top
done(lostX, lostY)
} else { done(e.offsetX, e.offsetY) }
})

只貼了部分關(guān)鍵代碼,完整代碼文末查看。

邊界判斷

如果不對(duì)邊界情況進(jìn)行處理可能會(huì)導(dǎo)致拖動(dòng)時(shí)發(fā)生意外的中斷,無(wú)法正確回收克隆元素。

// 鼠標(biāo)離開(kāi)了視窗
document.addEventListener("mouseleave", (e) {
end()
})
// 用戶(hù)可能離開(kāi)了瀏覽器
window.onblur = () {
end()
}

體驗(yàn)優(yōu)化

參考稿定設(shè)計(jì)中元素拖拽是直接賦值原圖的,原圖大小通常無(wú)法控制,免不了需要加載時(shí)間,造成卡頓空白的問(wèn)題,在網(wǎng)絡(luò)不夠快時(shí)體驗(yàn)尤其尷尬:

我的優(yōu)化思路是利用瀏覽器加載過(guò)同一張圖片就會(huì)優(yōu)先讀緩存的機(jī)制,先用一個(gè)Image加載原圖,等其加載完畢再把拖拽元素的src改成原圖,這樣瀏覽器會(huì)"自動(dòng)"幫我們優(yōu)化這個(gè)過(guò)程,只需要注意一點(diǎn),由于這是個(gè)異步任務(wù),所以一定要做好對(duì)應(yīng)標(biāo)記,不然手速快的時(shí)候控制不好觸發(fā)順序。

function simulate(url, flag) {
cloneEl.setAttribute('raw', url)
const image = new Image()
image.src = url
image.onload = function () {
// 異步任務(wù),克隆節(jié)點(diǎn)可能已不存在,flag標(biāo)記是否拖動(dòng)的還是當(dāng)前目標(biāo)
cloneEl && initial.flag === flag && (cloneEl.src = url)
}
}

效果演示,故意加大了圖片的分辨率差異:

以上就是文章的全部?jī)?nèi)容,感謝看到這里,希望對(duì)你有所幫助或啟發(fā)!創(chuàng)作不易,如果覺(jué)得文章寫(xiě)得不錯(cuò),可以點(diǎn)贊收藏支持一下,也歡迎關(guān)注,我會(huì)更新更多實(shí)用的前端知識(shí)與技巧,我是茶無(wú)味的一天,期待與你共同成長(zhǎng)~

相關(guān)鏈接

[1] 完整代碼地址: https://juejin.cn/post/7145447742515445791/#heading-9

?[2] 關(guān)于作者: https://book.palxp.com

責(zé)任編輯:武曉燕 來(lái)源: 今日頭條
相關(guān)推薦

2023-09-07 07:35:59

JS操作網(wǎng)頁(yè)

2022-04-27 16:24:59

iOS蘋(píng)果升級(jí)

2020-09-07 07:00:09

AI 數(shù)據(jù)人工智能

2024-05-21 10:28:51

API設(shè)計(jì)架構(gòu)

2023-03-03 17:00:00

部署Linux內(nèi)核

2012-10-10 10:22:57

JavaScriptJSjQuery

2025-03-03 12:00:00

JavaScriptfor 循環(huán)語(yǔ)言

2023-07-18 07:56:20

2021-05-10 20:58:11

數(shù)據(jù)庫(kù)擴(kuò)容用戶(hù)

2022-08-16 08:37:09

視頻插幀深度學(xué)習(xí)

2020-07-22 15:15:28

Vue前端代碼

2022-12-19 14:53:07

模型訓(xùn)練

2022-07-15 07:58:18

SteampipeCLI 工具

2024-05-16 12:03:54

Python代碼開(kāi)發(fā)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)