移動端的項目經常會引入手勢庫來實現拖拽 不過如果只是一兩個頁面用到拖拽,再引入一個手勢庫就很不划算 最近的項目中就有這麼一個需求: 因為就這一個地方需要拖拽,所以我就沒有引入第三方庫 移動端的拖拽有兩種主流的實現方案: 1. 將元素設置為固定定位,然後在拖拽的時候修改其定位,實現拖拽的效果; 這種 ...
移動端的項目經常會引入手勢庫來實現拖拽
不過如果只是一兩個頁面用到拖拽,再引入一個手勢庫就很不划算
最近的項目中就有這麼一個需求:
因為就這一個地方需要拖拽,所以我就沒有引入第三方庫
移動端的拖拽有兩種主流的實現方案:
1. 將元素設置為固定定位,然後在拖拽的時候修改其定位,實現拖拽的效果;
2. 使用 transform 中的平移 translate 屬性實現拖拽。
方案一:固定定位 fixed
這種方案的核心就是給元素添加固定定位 position:fixed;
但定位之後,元素會脫離文檔流,會影響原有但佈局
因此在開始拖拽 (觸發 touchstart 事件) 的時候,需要將原本的元素 A 拷貝一份 ( cloneNode() )
給新元素 A2 添加定位,同時給原本的元素 A 設置 visibility: hidden; 使之隱藏並占位
1.1 創建遮罩
首先封裝一個創建遮罩的方法,用於放置拷貝出來的元素,並防止誤觸
createModal (id) {
let modal = document.getElementById(id)
if (!modal) { // 在沒有遮罩的時候創建遮罩
modal = document.createElement('div')
modal.id = id
modal.style.cssText = `position: fixed; left: 0; top: 0; right: 0; bottom: 0; z-index: 999;`
document.body.appendChild(modal)
}
},
1.2 開始拖拽
在觸發 touchstart 事件的時候,首先創建遮罩
並通過 getBoundingClientRect() 方法獲取到元素 A 的坐標,記錄起點信息
為了記錄起點信息,需要 data 中創建一個對象 source,用於記錄點擊的位置 client,和初始定位坐標 start
handleTouchstart (e) { // 開始拖拽
// 創建遮罩層
this.createModal(this.modalID) // modalID 遮罩層的id,由外部定義
let element = e.targetTouches[0]
let target = e.target.cloneNode(true) // 拷貝目標元素
target.id = this.copyID // copyID 拷貝元素的id,由外部定義
// 記錄初始點擊位置 client,用於計算移動距離
this.source.client = {
x: element.clientX,
y: element.clientY
}
// 算出目標元素的固定位置
let disX = this.source.start.left = element.target.getBoundingClientRect().left
let disY = this.source.start.top = element.target.getBoundingClientRect().top
target.style.cssText = `position: fixed; left: ${disX}px; top: ${disY}px;`
// 將拷貝的元素放到遮罩中
document.getElementById(this.modalID).appendChild(target)
},
1.3 處理拖拽
拖拽的時候,監聽 touchmove 事件
用【當前滑鼠點位置】減去【初始點擊位置】得到移動的距離
再結合初始坐標信息,更新拖拽元素的坐標
handleTouchmove (e) { // 拖拽中
let element = e.targetTouches[0]
let target = document.getElementById(this.copyID)
// 根據初始點擊位置 client 計算移動距離
let left = this.source.start.left + element.clientX - this.source.client.x
let top = this.source.start.top + element.clientY - this.source.client.y
// 移動當前元素
target.style.left = `${left}px`
target.style.top = `${top}px`
},
1.4 拖拽結束
拖拽結束的時候,記錄終點位置,刪除遮罩
handleTouchend (e) { // 拖拽結束
let end = {
x: e.changedTouches[0].clientX,
y: e.changedTouches[0].clientY
}
// 刪除遮罩層
let modal = document.getElementById(this.modalID)
document.body.removeChild(modal)
// 處理結果
this.doingSth(end)
},
不過上面的代碼只實現了拖拽的功能,並沒有對目標元素 A 進行顯示/隱藏的操作
可以根據業務場景自行添加,或者參考方案二
方案二:平移動畫 translate
這種方案更為簡單,不需要創建額外的 DOM 元素
只需對原本的元素添加 transform 屬性,甚至不需要 transition 屬性
然後在拖拽過程中,實時更新 transform: translate(X, Y) 中 x, y 的坐標信息,實現拖拽
2.1 開始拖拽
開始拖拽的時候,只需要記錄起點坐標
handleTouchstart (e) { // 開始拖拽
let element = e.targetTouches[0]// 記錄初始 client 位置,用於計算移動距離
this.source.client = {
x: element.clientX,
y: element.clientY
}
},
為了防止拖拽的過程中誤觸,建議使用方案一的 createModal() 方法創建一個遮罩
2.2 處理拖拽
根據當前坐標和起點坐標,計算出距離,然後更新 translate 的坐標
handleTouchmove (e) { // 拖拽中
let element = e.targetTouches[0]
// 根據初始 client 位置計算移動距離
let x = element.clientX - this.source.client.x
let y = element.clientY - this.source.client.y
// 移動當前元素
element.target.style.cssText = `transform: translate(${x}px, ${y}px);`
},
2.3 拖拽結束
拖拽完成後,清除平移動畫
handleTouchend (e) { // 拖拽結束
// 清除拖拽樣式
e.target.style.cssText = `transform: none;`
},
小結:
方案一在獲取目標元素的坐標信息的時候使用了 getBoundingClientRect() 方法
但這個方法性能不高,應當少用
而且即時使用了該方法,最後得到的 left 和 top 也不夠精確,touchstart 的時候,元素有明顯的閃動
我的項目使用了 vue.js,但拖拽功能用到 vue 的地方不多,將幾個用於記錄的對象提出來,就能復用於其他框架