造輪子之圖片預覽組件(preview)

圖片放大預覽是種很常見的場景和功能,通常移動網站首頁的輪播 banner,商品詳情頁的商品圖片等位置都會用到此功能css

像這種經常使用的場景功能確定是有人早就寫好插件了的,因此遇到這種場景,通常都遵循如下三步:html

  • 打開冰箱 啓動 Github
  • 搜索 photopreviewcarouselphotoSwipe等關鍵字
  • 找到想要的庫,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事件比較複雜,而且涉及到不一樣 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的值有點不太對勁,並且這種方法的計算方式也比較複雜,擔憂影響性能

仔細想了下,也是能夠解決的

之因此第二次縮放會產生跳動,就在因而改變了第一次結束後的狀態,由於這個狀態並非固定的,此時圖片的 scaletransform-origin都是被動態修改過的,只要能把這個狀態給固定下來,固定爲默認狀態的值,那不就好了嗎?

至於如何固定這個狀態,其實也是很簡單的

對於一個尺寸爲 100*100的圖片,以 (10, 10)transform-origin放大 2倍,則放大後的圖片尺寸爲 200*200,左上角偏移量爲 (-10, -10) SO 在第一次縮放結束後,當即將圖片的寬高設置爲 200*200,而且給個 left: -10; top: -10的偏移量,而後就能夠將 scaletransform-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距離,動態位移圖片位置便可

不過爲了更貼近實際的物理交互,達到更好的用戶體驗,添加了一個慣性滑動的能力,即當用戶在滑動圖片的過程當中結束觸摸時,圖片還會繼續往前滑動必定的距離

這個場景有兩種解決方案

  • css 動畫

在觸摸結束的瞬間,以當前速度爲條件,計算出圖片應該滑動多少距離才停下來,並設置一個速度逐漸下降的 transition動畫

  • js 動態動畫

規定一個速度遞減的係數,每一幀的速度都在前一幀的基礎上,以這個係數爲前提進行遞減,直到最後停下來

綜合考慮了一下,第一種的方式可能更加節約性能,可是不太好模擬出那種物理慣性的感受,數值不太好計算,相比於節約的那一點性能來講,性價比不高

第二種方式更加容易控制,因此選擇了第二種方式

主要是藉助了 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的疼

源碼已經放到 github上了,代碼註釋得也算是比較詳細,感興趣的能夠參考下,若是有什麼問題,歡迎提 issues

相關文章
相關標籤/搜索