原生JS手寫(xiě)絲滑流暢的元素拖拽效果
前言
提到元素拖拽,通常都會(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 效果:
利用濾鏡 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):
將鼠標(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)了:
上面只是實(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):
最終我在動(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):
實(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):
效果演示:
注意元素都要設(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à)
- 刪除元素
判斷是否在畫(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à)布中的位置了。
只貼了部分關(guān)鍵代碼,完整代碼文末查看。
邊界判斷
如果不對(duì)邊界情況進(jìn)行處理可能會(huì)導(dǎo)致拖動(dòng)時(shí)發(fā)生意外的中斷,無(wú)法正確回收克隆元素。
體驗(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ā)順序。
效果演示,故意加大了圖片的分辨率差異:
以上就是文章的全部?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