2022強力之作:一款超精緻的圖片預覽元件

語言: CN / TW / HK

theme: github

我剛接觸前端這個行業的時候就有一個想法,那就是寫一個超炫酷的圖片預覽畫廊。還記得當時用美圖看看看,那輕快的速度和互動很是令人著迷,就想著自己寫一個。

該元件在幾年前已經發布不完全版,後面斷斷續續的維護,總感覺差了點什麼。今年春節沒休息,全搭在上面進行開發,現在總算是完美實現!先看看效果:

提前預覽:https://react-photo-view.vercel.app/

縮圖完美漸變:

1.gif

指定位置放大:

2.gif

減速滾動:

3.gif

什麼是 react-photo-view

react-photo-view 擁有無與倫比的預覽互動體驗:從開啟影象開始,每一幀的動畫、細節和互動都經過了精心設計與反覆除錯,媲美原生圖片預覽的效果。

bash pnpm i react-photo-view

概覽:

```js import { PhotoProvider, PhotoView } from 'react-photo-view'; import 'react-photo-view/dist/react-photo-view.css';

export default function MyComponent() { return ( ); } ```

為什麼要單獨開發它?

當然想實現它的執念也算一個方面,但根本原因是在 React 強大的生態中根本找不到一個好用的圖片預覽方案。當時奉行拿來主義,在網上找了一圈基於 React 放大預覽元件庫,結果令我有點意外,圖片放大預覽的庫的數量明顯比不上輪播元件庫。更令人窒息的是這些少得可憐的元件庫中,其中一大半都是基於 PhotoSwipe 這個開源庫進行的二次封裝。除此之外,能用於實際生產的預覽元件庫……好像沒有(也可能是我找不到),這種情況不僅體現在 React 庫上,其他框架 Vue 乃至是原生的相關庫都是如此。

當然 PhotoSwipe 也不是不能用,但原生操作 DOM 的寫法在 React 中格格不入,其體積也是在 gzip 12KB 之上了,顯得有點臃腫了,便有了這個大膽的想法。

它有多優秀?

它擁有非常完善的細節與特性:

  • 支援觸控手勢,拖動/平移/物理效果滑動,雙指指定位置放大/縮小
  • 全方面動畫銜接,開啟/關閉/回彈/觸邊,順其自然的互動效果
  • 影象自適應,以一個合適的最初呈現大小,並根據調整自適應
  • 支援自定義如 <video /> 或任意 HTML 元素的預覽
  • 鍵盤導航,完美適配桌面端
  • 支援自定義節點擴充套件,輕鬆實現全屏預覽、旋轉控制、圖片介紹以及更多功能
  • 基於 typescript7KB Gzipped,支援服務端渲染
  • 簡單易用的 API,上手零成本

還匯出了支援 ES2017 以上的 JS,可以做到 6KB Gzipped。在如此的體積上加上非常多的體驗細節實屬不容易,更多的功能可以通過非常容易的自定義渲染來實現,這與 React 理念完美契合,從而可以避免內建一些非剛需的功能。

流行庫對比

以下表格統計了大部分場景所需功能,展示 react-photo-viewPhotoSwiperc-image(ant-design) 對比:

| | react-photo-view | PhotoSwipe | rc-image | | ------------------ | ---------------- | -------------------- | ------------ | | MINIFIED | 19KB | 47KB | 40KB | | MINIFIED + GZIPPED | 7.3KB | 12KB | 14KB | | 基礎預覽 | 支援 | 支援 | 支援 | | 切換預覽 | 支援 | 支援 | 不支援 | | 移動端 | 支援 | 支援 | 不支援 | | 縮圖完美漸變 | 支援 | 支援 | 不支援 | | 縮圖裁切動畫 | 支援 | 支援(需手動指定) | 不支援 | | 自適應影象尺寸 | 支援 | 不支援(需手動指定) | 支援 | | fallback | 支援 | 不支援 | 支援 | | 滑鼠滾輪縮放 | 支援 | 不支援 | (缺少位置) | | 彈簧物理滾動 | 支援 | 支援 | 不支援 | | 動畫引數調整 | 支援 | 支援 | 不支援 | | 易用 API | 支援 | 不支援 | 支援 | | TypeScript | 支援 | 不支援 | 支援 | | 鍵盤導航 | 支援 | 支援 | 支援 | | 自定義元素 | 支援 | 存在 XSS 風險 | 不支援 | | 受控元件 | 支援 | 支援 | 支援 | | 迴圈預覽 | 支援 | 支援 | 不支援 | | 圖片旋轉 | 支援 | 不支援 | 支援 | | 自定義工具欄 | 支援 | 支援 | 不支援 | | 原生全屏開啟 | 自定義擴充套件 | 支援 | 不支援 |

友好的文件

還有什麼比文件更重要了,為此,我還準備了一個超漂亮的文件(目前只有中文,以後有時間在翻譯吧~)

https://react-photo-view.vercel.app/

4.png

實現歷程

圖片跟隨手指滾動

onTouchStart 時記錄當前觸發位置狀態,在 onTouchMove 時讓其跟隨手指移動,onTouchEnd 解除跟隨就可以簡單實現。

觸邊位置反饋使圖片切換都是需要慢慢琢磨細節:在 onTouchStart 之後移動如果立即讓圖片跟隨手指移動的話會帶來許多誤操作,比如本想讓他切換圖片卻走了上下滑動的邏輯。這時候就需要一個 20px 的移動緩衝來預判手指移動方向。

指定圖片位置進行放大

使用 transform: scale(value) 可以實現對圖片的縮放,但是都是對圖片中心進行放大,縮放的結果可能不是想要的。起初打算用 transform-origin 來實現,想法是美好的,雖然第一次在指定的位置能夠進行放大。倘若縮小的位置不是原來的位置就會產生混亂跳動,很顯然這個方式不行。

後來思來想去睡不著,在睡夢中發現了靈感:便於計算理解,我們設圖片中心點為 0任何指定位置的放大縮小,即改變圖片中心的位置。比如圖片寬度 200,中心點位置為 100,基於最左側位置放大一倍。現在圖片寬度 400,那麼中心點的位置應為 200。那麼總結公式如下:

js const centerClientX = innerWidth / 2; // 座標偏移轉換 const lastPositionX = centerClientX + lastX; // 縮放偏移 const offsetScale = nextScale / scale; // 最終偏移位置 const originX = clientX - (clientX - lastPositionX) * offsetScale - centerClientX;

這種模式計算能承擔各種位置響應,比如雙指縮放、雙指滾動+縮放、邊緣計算等等。

雙指之間的距離

這裡需要初中時直角三角勾股定理:

js Math.sqrt((nextClientX - clientX) ** 2 + (nextClientY - clientY) ** 2);

模擬滾動操作

之前的版本使用 transition 實現,通過手指滑動開始結束的時間差,計算出初始速度,估摸著用 transition 模擬出一個距離讓眼睛看起來有滾動效果 😂。但這種方式體驗始終差很多。後面結合高中物理公式模擬出滾動效果:

5.png

加速運動:

6.png

空氣阻力:

7.png

CρS 都是常數,乾脆都搞成一個量好了。至於怎麼出這個量大小……試出來的 😂 這樣就只與 v 平方成正比了。

另外因為和運動方向相反,取個 v 的方向即 Math.sign(-v)

```ts function scrollMove( initialSpeed: number, callback: (spatial: number) => boolean, ) { // 加速度 const acceleration = -0.002; // 阻力 const resistance = 0.0002;

let v = initialSpeed; let s = 0; let lastTime: number | undefined = undefined; let frameId = 0;

const calcMove = (now: number) => { if (!lastTime) { lastTime = now; } const dt = now - lastTime; const direction = Math.sign(initialSpeed); const a = direction * acceleration; const f = Math.sign(-v) * v 2 * resistance; const ds = v * dt + ((a + f) * dt 2) / 2; v = v + (a + f) * dt;

s = s + ds;
// move to s
lastTime = now;

if (direction * v <= 0) {
  cancelAnimationFrame(frameId);
  return;
}

if (callback(s)) {
  frameId = requestAnimationFrame(calcMove);
  return;
}
cancelAnimationFrame(frameId);

}; frameId = requestAnimationFrame(calcMove); } ```

縮圖裁切

PhotoSwipe 支援縮圖裁切,不過需要手動指定圖片寬高和 data-cropped,相當麻煩。react-photo-view 通過讀取縮圖 getComputedStyle(element).objectFit 來獲取當前裁切引數。實現自動裁切效果。

4.gif

相容性處理

因為每張圖片都是一個合成層,這會消耗相當多的記憶體。IOS 上對於記憶體有相當大的限制,如果圖片在放大的情況一直使用 scale,那麼在 Safari 上會顯得非常模糊。現在通過每次在運動完成後,都改變圖片的寬高為指定的值,然後重設 scale 為 1,這種方式應該本身需要達到的效果吧。

其他

PhotoSwipe 的作者是居住在基輔的烏克蘭人,他逃離了基輔,現在他和他家人在烏克蘭西部很安全,也希望在戰爭結束後他能重新振作起來。

結語

我在 react-photo-view 的細節上面花費了相當的精力,如果喜歡的話可以幫忙點個 Star 就是對我的支援,謝謝!

  • GitHub: https://github.com/MinJieLiu/react-photo-view
  • Gitee: https://gitee.com/MinJieLiu/react-photo-view

今年輸出的文章