圖片放大預覽是種很常見的場景和功能,通常移動網站首頁的輪播 banner
,商品詳情頁的商品圖片等位置都會用到此功能css
像這種經常使用的場景功能確定是有人早就寫好插件了的,因此遇到這種場景,通常都遵循如下三步:html
photo
、preview
、carousel
、photoSwipe
等關鍵字npm install
之這種作法沒毛病,有現成的輪子可用固然拿來主義,由於項目用的是 vue
,因此我在網上找了一圈 基於 vue
的放大預覽組件庫,結果令我有點意外,圖片放大預覽的庫的數量明顯比不上輪播組件庫,而且更使人 智熄 的是,這些少得可憐的組件庫中,其中一大半都是基於 PhotoSwipe 這個開源庫進行的二次封裝,除此以外,能用於實際生產的預覽組件庫(image gallery
)……好像沒有(也多是我見識短淺),這種狀況不只體如今 vue
庫上,其餘框架乃至是原生的相關庫都是如此vue
雖然說不提倡重複造輪子,但輪子太少沒有選擇的餘地也有點說不過去, PhotoSwipe 用起來很順手,功能也很齊全,足以應對實際生產環境中的絕大部分場景git
但與此同時,也就表明它代碼體積會比較大,引入的冗餘代碼會比較多,因而,抱着精簡代碼以及順便豐富放大預覽插件家族的想法,決定本身造個輪子github
先看下最終實現效果:npm
或者你想本身體驗一下,這裏也有個寫好的 Demobash
我已經將此功能打包成了一個
npm package
,可直接下載安裝使用,包括樣式在內的代碼體積壓縮後不到22KB
,Gzipped以後不到8KB
,源碼 已上傳框架
滑動形式的選型與 造輪子之圖片輪播組件(swiper)中的同樣,就很少說了post
數據處理和 造輪子之圖片輪播組件(swiper) 中的第一種方法同樣,就很少說了:性能
<VueActivePreview :urlList="urlList" />
複製代碼
此組件的 touch
事件比較複雜,而且涉及到不一樣 touch
事件之間的交互,因此稍微麻煩點,不過只要條理清晰,考慮清晰,仍是能夠解決的
單指滑動的主體邏輯與 造輪子之圖片輪播組件(swiper)的相差很少,都是計算手指滑動的距離,經過不斷改變 translate
的值進行位移
支持對單個圖片的縮放操做,原理其實很簡單,經過計算在起始時與滑動過程當中雙指間距離的比例,就能夠獲得圖片的縮放比例
獲取雙指間距離:
getDistance (p1, p2) {
return Math.sqrt(Math.pow(p2.clientX - p1.clientX, 2) + Math.pow(p2.clientY - p1.clientY, 2))
}
複製代碼
獲取圖片縮放比例:
this.scaleValue = this.getDistance(targetTouch1, targetTouch2) / doubleTransferInfo.startDistance
複製代碼
經過改變 transform: scale(scaleValue)
,就能夠實現圖片的即時縮放
不過,這個時候有個問題,那就是 CSS3 scale
的縮放中心座標,默認是 50% 50% 0
,也就是元素的中心位置,因此若是在不設置 transform-origin
的狀況下,直接設置 scale
,那麼圖片也能夠正常縮放,但縮放的結果卻並不必定是所想要的
好比,雙指中心座標是 (10, 56)
,按照正常習慣,當進行放大時,整張圖片應該是以這個點爲中心進行放大,而不該該是圖片的中心的位置
有兩種方法能夠解決這個問題
transform-origin
直接將雙指之間的中心座標設置爲 transform-origin
,而後進行縮放便可,這是最簡單的方法
因此須要動態設置 transform-origin
const targetTouch1 = e.touches[0]
const targetTouch2 = e.touches[1]
this.transOriginX = (targetTouch1.clientX + targetTouch2.clientX) / 2 - this.left
this.transOriginY = (targetTouch1.clientY + targetTouch2.clientY) / 2 - this.top
複製代碼
transform-origin
的改變,其實就是改變了圖片的位置狀態,無需關心 transform-origin
到底應該是什麼,直接默認圖片的中心位置就是每次圖片縮放的 transform-origin
,而後在圖片縮放的過程當中,動態地修正圖片的位置,抵消 transform-origin
帶來的影響,就可保持視覺上的統一
例如,圖片的默認 transform-origin
是 (100, 100)
,若是以此爲中心放大兩倍,那麼結束放大後,圖片的左上角相比於原始狀態向左偏移了 100
個單位,可是如今雙指的起始中心座標是 (0, 0)
(只是個假設,爲了方便計算說明),並將此設置爲 transform-rogin
的話,放大兩倍後,圖片的左上角相比於原始狀態向左將偏移 0
個單位,也就是沒有任何偏移
因此,當縮放中心是 (0, 0)
時,在不改變 transform-origin
的狀況下,要想保持視覺上的統一,必須在圖片放大的過程當中,將圖片進行持續地右移,保證在每一幀中移動的距離都能抵消由於 transform-origin
帶來的差距,直到最終移出 100
個單位
由於考慮到後面圖片的位置座標還有其餘地方須要用到,並且直接設置 transform-origin
的方式更簡單方便,因此這裏我選擇了第一種方法
but
,很快我就發現,我仍是想得太簡單了
假設如今手指離開屏幕後,圖片以 (10, 56)
爲縮放中心放大了 2
倍,而後雙指再次放在屏幕上,這個時候雙指的中心座標爲 (70, 88)
,這個時候按照上面說的,就須要動態地將 transform-origin
由以前的 (10, 56)
改成 (70, 88)
,可是若是真的改了,你就會發現,圖片馬上產生了跳動
這是由於在第二次雙指觸摸屏幕以前,圖片放大兩倍的狀態是基於 transform-origin(10, 56)
,如今改變了 transform-origin
,那就至關因而改變了圖片的放大基點,圖片的狀態必然會改變
難道要換成第二種方法?可是總感受頻繁地修改 left/top
的值有點不太對勁,並且這種方法的計算方式也比較複雜,擔憂影響性能
仔細想了下,也是能夠解決的
之因此第二次縮放會產生跳動,就在因而改變了第一次結束後的狀態,由於這個狀態並非固定的,此時圖片的 scale
和 transform-origin
都是被動態修改過的,只要能把這個狀態給固定下來,固定爲默認狀態的值,那不就好了嗎?
至於如何固定這個狀態,其實也是很簡單的
對於一個尺寸爲 100*100
的圖片,以 (10, 10)
爲 transform-origin
放大 2
倍,則放大後的圖片尺寸爲 200*200
,左上角偏移量爲 (-10, -10)
SO
在第一次縮放結束後,當即將圖片的寬高設置爲 200*200
,而且給個 left: -10; top: -10
的偏移量,而後就能夠將 scale
與 transform-origin
恢復到默認狀態了,這個時候的圖片狀態也就至關因而沒有使用任何 transform
屬性
那麼第二次縮放的時候,初始狀態就以當前這個 200*200
的圖片爲起始狀態而非是一開始的 100*100
,這樣一來,就無需關心狀態問題了,由於每一次縮放都是一個全新的狀態
直觀示例:
transform: scale(2); => width: 200; height: 200;
transform-origin: 10 10; => left: -10; top: -10;
複製代碼
代碼示例:
this.left = left
this.top = top
this.currentW = currentW
this.currentH = currentH
this.scaleValue = 1
複製代碼
單個圖片縮放後,爲了容許用戶更自由地查看圖片的每一個細節,容許對縮放後的圖片進行滑動查看
這個功能的主體邏輯仍是比較簡單的,經過監聽 touch
事件,計算獲得每一幀間的 move
距離,動態位移圖片位置便可
不過爲了更貼近實際的物理交互,達到更好的用戶體驗,添加了一個慣性滑動的能力,即當用戶在滑動圖片的過程當中結束觸摸時,圖片還會繼續往前滑動必定的距離
這個場景有兩種解決方案
在觸摸結束的瞬間,以當前速度爲條件,計算出圖片應該滑動多少距離才停下來,並設置一個速度逐漸下降的 transition
動畫
規定一個速度遞減的係數,每一幀的速度都在前一幀的基礎上,以這個係數爲前提進行遞減,直到最後停下來
綜合考慮了一下,第一種的方式可能更加節約性能,可是不太好模擬出那種物理慣性的感受,數值不太好計算,相比於節約的那一點性能來講,性價比不高
第二種方式更加容易控制,因此選擇了第二種方式
主要是藉助了 requestAnimationFrame
這個 API
(已經對不兼容此 API
的設備作了降級處理):
rafHandler = raf(() => {
speedX *= 0.9
speedY *= 0.9
// ...
if (Math.abs(speedX) < 1) speedX = 0
if (Math.abs(speedY) < 1) speedY = 0
if (speedX !== 0 || speedY !== 0) {
this.frictionMove(speedX, speedY)
} else {
// ...
}
})
複製代碼
其實這個組件的主體邏輯仍是蠻清晰的,沒什麼太多的道道,可是須要考慮的狀況太多,並且還有三種不一樣狀況下 touch
事件的交互與判斷,全部的狀況綜合在一塊兒仍是蠻傷腦筋的,五分之一不到的時間用來寫主體邏輯,剩下的時間全耗在 if...else
上了,等我把這個輪子寫完,我也算是明白爲什麼這個場景的輪子那麼少了,由於真的腦闊疼,不是功能邏輯的疼,功能邏輯寫起來畢竟還有點意思,而是 if...else
的疼