使用 React Hooks 實現仿石墨的圖片預覽插件(巨詳細)

前言

最近工做中須要製做一個圖片預覽的插件,在參考了不少產品(掘金、知乎、簡書、石墨等)的圖片預覽以後,最終仍是以爲石墨的比較符合咱們的產品需求。javascript

原本覺得能在社區中找到相關插件,但想法美好,現實卻很骨感,因而便決定本身手擼一個,順便也學習一下組件的開發流程。html

 

項目介紹

項目預覽圖

項目最終的實現效果以下圖,基本上跟石墨的圖片預覽是一毛同樣的。支持 放大圖片縮小圖片原尺寸大小顯示圖片適應屏幕下載圖片(這個還在開發中),也就是底部欄的五個操做按鈕。java

技術棧

組件是基於 React HooksTypeScript 實現的,打包工具使用的是 webpacknode

本篇文章對 webpack 的配置不會作相應的介紹,若是你們對 webpack 感興趣的話,能夠參考筆者整理的 淺談 Webpack 性能優化(內附巨詳細 Webpack 學習筆記)

項目目錄

.
├── node_modules // 第三方的依賴
├── config        // webpack 配置文件夾
    ├── webpack.base.js     // webpack 公共配置文件
    ├── webpack.dev.config.js  // 開發環境配置文件
    └── webpack.prod.config.js  // 生產環境配置文件
├── example    // 開發時預覽代碼
    ├── src    // 示例代碼目錄
      ├── app.js     // 測試項目 入口 js 文件
      └── index.less // 測試項目 入口 樣式文件 文件
├── src        // 組件源代碼目錄
    ├── components     // 輪子的目錄
      ├── photoGallery // photoGallery 組件文件夾
    ├── types  // typescripe 的接口定義
    ├── utils  // 工具函數目錄
    ├── images  // 圖片文件目錄
    ├── index.html  // 項目入口模版文件
    ├── index.tsx      // 項目入口文件
    └── index.less   // 項目入口樣式文件
├── lib  // 組件打包結果目錄
├── .babelrc // babel 配置文件
├── .gitignore // git上傳時忽略的文件
├── .npmignore // npm 上傳忽略文件
├── README.md
├── tslint.json // tslint 的配置文件
├── tsconfig.json // ts 的配置文件
├── package-lock.json    // yarn lock 文件
└── package.json // 當前整一個項目的依賴

倉庫地址

倉庫地址在此:仿石墨的圖片預覽插件react

 

思路分析

此插件的核心在於圖片的展現,以及圍繞對預覽圖片進行的操做,如 放大縮小適應屏幕,而這幾個操做又都是跟圖片的尺寸有關的,其實咱們只要知道在點擊相應操做的按鈕的時候,圖片應該顯示多大的尺寸,整個問題就解決了。webpack

因而筆者就研究了一波其背後預覽邏輯,發現了幾個對編碼比較有用的點:git

首先圖片不能一直放大和縮小,它一定有一個最大值和最小值,操做了一波發現石墨中 預覽圖片的最大值是原圖的 4 倍最小值是原圖的 10 倍,與此同時還須要規定從原圖開始點擊幾回到最大值或者最小值,在插件中我規定的次數是 6 次。github

這樣在圖片加載完成以後,咱們能很方便的算出這張預覽圖片的全部尺寸,能夠將這些尺寸維護在一個數組中,這樣在每個放大縮小的點擊背後,都會有一個圖片尺寸與其對應。web

接着咱們須要知道的是當前預覽圖片的顯示尺寸位於 尺寸數組 中的哪個 index,有了這個 index 以後,咱們就只須要取出這個 index 對應的圖片寬度進行繪製便可。算法

這裏就涉及到圖片首次在容器中的顯示狀況了,咱們拿長圖舉例:長圖預覽,插件會在圖片上下兩側留出必定的間距,這個間距實際上是固定的,在石墨中我算了一下,上下兩側留出的間隙各是容器高度的 5%,具體能夠看下圖(原諒圖片很魔性),圖中 A 的距離是 B5%

這樣咱們能夠計算出當前圖片的尺寸,拿這個尺寸去 尺寸數組 中找到與這個值最爲接近的值,這個最接近的值的索引就是當前預覽圖片的 index 值。

還有一個石墨的預覽圖片是經過 canvas 畫上去的,咱們這裏也會使用 canvasdrawImage 這個 api 來進行圖片的繪製,固然在不支持 canvas 的瀏覽器上,咱們就直接使用 <img /> 標籤。

在本文就主要分析 canvas 畫圖這一塊內容, <img /> 標籤其實也是相似的。

到這裏基本上此插件的難點都已經解決了,接下來咱們就開始分析相應的代碼。

 

代碼分析

插件接收參數

首先咱們來看一下插件的所須要的參數,大體能夠歸爲下面幾個:

  • visible:控制預覽插件的顯示隱藏
  • imgData:須要預覽的圖片數組
  • currentImg:再打開預覽插件的時候,默認顯示第幾張圖
  • hideModal:預覽插件的關閉方法

筆者能想到的暫時就這四個,基本上其實也已經夠用了,使用以下:

<PhotoGallery
  visible={visible}
  imgData={ImgData}
  currentImg = {9}
  hideModal={
    () => {
      setVisible(false);
    }
  }
/>

 

插件結構

插件的結構其實很簡單,其實就三塊:圖片顯示塊圖片列表選擇側邊欄底部操做塊,定義爲三個子組件塊:分別爲 <Canvas /><Sidebar /><Footer /> ,統一由一個父組件管理。

由於咱們主要講解 canvas 畫圖片,因此圖片顯示塊就設置爲 <Canvas />,不支持的 canvas 的瀏覽器,在源碼中會使用 <Image /> 組件來進行圖片展現,這裏就不作具體介紹了,你們能夠參考源碼。

父組件代碼以下:

// src/components/photoGallery/index.tsx

import React, { useState }  from 'react';
import classNames from 'classnames';
import { Footer, Sidebar, Canvas } from './components';

const photoGallery = (props: Props): JSX.Element => {
  const { imgData, currentImg, visible } = props;
  
  // 當前顯示第幾張圖片
  const [currentImgIndex, setCurrentImgIndex] = useState(currentImg);

  return (
    <div
      className={
        classNames(
          styles.modalWrapper,
          {
            [styles.showImgGallery]: visible, // 根據 visible 渲染插件
          }
        )
      }
    >
      <div className={styles.contentWrapper}>
        <Canvas
          // 要加載的圖片 url
          imgUrl={imgUrl}
        />
      </div>
      <Sidebar
        // 圖片數組
        imgData={imgData}
      />
      <Footer
        // 圖片數量
        imgsLens={imgData.length}
        // 當前第幾張
        currentImgIndex={currentImgIndex}
      />
    </div>
  );
}

如上圖所示,這樣插件的大體的結構就算完成了,接下來就是最核心的圖片顯示模塊的邏輯。

 

圖片預覽核心邏輯

咱們先建立一個類 canvas.ts,對於圖片的預覽操做,咱們都在這個類中進行操做。

這個類接受兩個參數,一個是渲染的容器 dom,另一個就是實例化所須要用到的參數 options,下面是 options 的接口實現:

interface CanvasOptions {
  imgUrl: string; // 圖片地址
  winWidth: number; // 屏幕寬度
  winHeight: number; // 屏幕高度
  canUseCanvas: boolean; // 瀏覽器是否可使用 canUseCanvas
  loadingComplete?(instance: any): void; // 製做圖片 loading 效果
}

還有咱們會講一系列跟預覽圖片有關的屬性都掛在其實例屬性上,如:

  • el:渲染的容器
  • canUseCanvas:是否支持 canvas,決定以什麼方式畫圖
  • contextcanvas 的畫布 getContext('2d')
  • image:預覽圖片對象
  • imgUrl:預覽圖片 url
  • imgTop:圖片右上角目標 canvasy 軸的高度
  • imgLeft:圖片右上角目標 canvasx 軸的高度
  • LongImgTop:圖片距離容器頂部的距離,用於圖片滾動和拖動
  • LongImgLeft:圖片距離容器左側的距離,用於圖片滾動和拖動
  • sidebarWidth:側邊欄的寬度
  • footerHeight:底部欄的高度
  • cImgWidth:畫布中圖片的寬度
  • cImgHeight:畫布中圖片的高度
  • winWidth:屏幕的寬度
  • winHeight:屏幕的高度
  • curPos:鼠標拖動圖片是須要用的的 x/y
  • curScaleIndex:當前顯示圖片,位於尺寸數組中的哪個 index
  • fixScreenSize:使用屏幕大小的尺寸數組中的 index
  • EachSizeWidthArray:圖片的尺寸數組,包含了放大縮小全部尺寸的寬度值
  • isDoCallback:圖片是否加載完成

插件中使用的屬性值基本上都在上面了。

 

先畫一張簡單的圖

首先咱們先來看一下這個 canvas 畫圖的這個 api,它能幫助咱們在畫布上繪製圖像、畫布或視頻。

咱們能夠經過下面的方法放大來幫咱們畫出一張圖片:

var c = document.getElementById("myCanvas");
// 建立畫布
var ctx = c.getContext("2d");
// 開始繪製
ctx.drawImage(image, dx, dy, dWidth, dHeight);

其中參數的意思分別爲:

  • image:規定要使用的圖像、畫布或視頻。
  • dximage 的左上角在目標 canvasX 軸座標
  • dyimage的左上角在目標 canvasy 軸座標
  • dWidthimage 在目標 canvas 上繪製的寬度。
  • dHeightimage 在目標 canvas 上繪製的高度。

具體能夠看下圖:

關於此方法更多用法你們能夠參考:drawImage 的 MDN 文檔

有了這個 api 以後,咱們其實只要計算出這個 api 對應的 5 個參數便可,舉個簡單的例子,下面這張圖咱們改怎麼獲得 5 個參數:

  • image 對象

咱們可使用 new Image() 來實例化一個 image 對象,並指定他的 src 屬性爲相應的圖片 url 地址,這樣就能夠獲得一個 image 對象,當圖片加載完成以後,咱們就能夠經過 imgDom.naturalWidthimgDom.naturalHeight 圖片的原始寬高:

// src/components/photoGallery/canvas.ts

loadimg(imgurl) {
  const imgDom = new Image();
  imgDom.src = imgUrl;

  imgDom.onload = function() {
    // 圖片加載完成以後
    // 作你想要作的事情
  }
}
  • dxdydwidthdHeight 屬性

咱們以長圖舉例:咱們在講解思路的時候分析過,上下兩邊留空的部分是 圖片顯示容器高度5%,在這裏咱們定義了底部塊的高度(footerHeight)爲 50px,側邊欄的寬度(sidebarWidth)爲 120px,這就變成了一道小學應用題,咱們能夠經過 window.innerWidthwindow.innerHeight 來獲得屏幕的寬(winWidth)和高(winHeight),通過計算咱們即可以獲得咱們所需的四個屬性:

/**
 * winWidth:屏幕寬度
 * winHeight:屏幕高度
 * footerHeight:底部高度
 * sidebarWidth:側邊欄寬度
 * wrapperWidth:圖片顯示區域寬度
 * wrapperHeight:圖片顯示區域高度
 * naturalWidth: 圖片原始寬度
 * naturalHeight: 圖片原始高度
 */

wrapperHeight = winHeight - footerHeight;
wrapperWidth = winWidth - sidebarWidth;

dy = wrapperHeight * 0.05;
dHeight = wrapperHeight - 2 * dy;

// 與原始寬高有個等比例的關係
dWidth = naturalWidth * dHeight / naturalHeight;
dx = (wrapperWidth - dWidth) / 2

上面就是計算咱們所需五個屬性的過程,總的來講仍是比較方便的。

因此在咱們每次要繪製圖片的時候,只要計算出這 5 個值就 ok 了。

 

初始圖片寬高

咱們在 utils 下的 img.ts 中定義一個方法 getBoundingClientRect,用來獲得 圖片的顯示寬高和他距離容器頂部的 imgTop、以及距離左側的 imgLeft

// src/utils/img.ts
/**
 * 返回第一次加載圖片的寬高,和 imgTop/imgLeft
 * 經過返回的參數 直接 經過 drawImage 畫圖了
 **/
export const getBoundingClientRect = (options: RectWidth): BoundingClientRect => {
  const {
    naturalWidth, // 圖片原始寬
    naturalHeight, // 圖片原始高
    wrapperWidth, // 顯示容器寬
    wrapperHeight, // 顯示容器高
    winWidth, // 屏幕寬度
  } = options;

  // 圖片寬高比
  const imageRadio = naturalWidth / naturalHeight;
  
  // 顯示容器寬高比
  const wrapperRadio = wrapperWidth / wrapperHeight;

  // 長圖的邏輯
  if (imageRadio <= 1) {
    // 具體畫布上方默認是 容器高度的 0.05
    imgTop = wrapperHeight * 0.05;

    // 圖片的高度
    ImgHeight = wrapperHeight - wrapperHeight * 0.05 * 2;
    // 根據原始寬高,等比例獲得圖片寬度
    ImgWidth = ImgHeight * naturalWidth / naturalHeight;

    // 若是圖片的寬高比顯示容器的寬高比大
    // 說明圖片左右兩側的寬度須要固定爲容器的寬度的 0.05 倍了
    if (wrapperRadio <= imageRadio) {
      ImgWidth = wrapperWidth - wrapperWidth * 0.05 * 2;
      ImgHeight =  ImgWidth * naturalHeight / naturalWidth;

      imgTop = (wrapperHeight - ImgHeight) / 2
    }

    // ...
    imgLeft = newWinWidth - ImgWidth / 2;
  }

  // 處理寬圖的邏輯
  // ...

  // 返回
  return {
    imgLeft,
    imgTop,
    ImgWidth,
    ImgHeight,
  }
}

更詳細的代碼你們能夠參考源碼。

 

預覽圖片尺寸數組

咱們在以前提到,咱們能夠把圖片放大縮小過程當中全部的尺寸都放到一個數組中去,方便以後經過索引去獲得相應的圖片尺寸,那麼怎麼進行操做呢?

其實只要在圖片加載完成以後,獲得圖片的原始寬高,經過原始寬高,經過相應的計算公式,計算獲得相應的尺寸數組,塞入數組便可。

在類中定義一個 setEachSizeArr 實例方法:

// src/components/photoGallery/canvas.ts
/**
 * 計算圖片放大、縮小各尺寸的大小數組,
 */
private setEachSizeArr () {
  const image = this.image;
  
  // 獲得尺寸數組
  const EachSizeWidthArray: number[] = getEachSizeWidthArray({
    naturalWidth: image.width,
    naturalHeight: image.height,
  })

  // 掛到實例屬性上去
  this.EachSizeWidthArray = EachSizeWidthArray;

  // 獲得適應屏幕的 index
  // 也就是操做按鈕中的 第四個按鈕
  const fixScreenSize = getFixScreenIndex({
    naturalWidth: image.width,
    naturalHeight: image.height,
    wrapperWidth: this.cWidth,
    wrapperHeight: this.cHeight,
  }, EachSizeWidthArray);

  // 將適應屏幕的 index 掛到實例屬性
  this.fixScreenSize = fixScreenSize;
}
  • getEachSizeWidthArray

咱們經過此方法獲得尺寸數組,由於最大的圖片是原圖的 4 倍,最小的圖片是原圖的 1/10,從最小到原圖 和 從原圖到最大 都須要通過 6 次,咱們能夠根據比例得出每個尺寸的大小,具體的代碼筆者就不貼了。

  • getFixScreenIndex

咱們經過此方法獲得適應屏幕的尺寸數組的 index,原理就是在尺寸數組中第一個寬高小於顯示容器寬高的 index

這兩個方法的具體代碼筆者就不貼了,你們有興趣能夠去源碼查看。

 

初始預覽圖片索引

咱們要計算出首次圖片渲染出來時候,位於尺寸數組的那一個 index,由於咱們獲得首次渲染圖片的寬度,能夠拿這個寬度去與尺寸數組中數組進行比對,最接近的這個值的索引 index,就是當前圖片的 index 值:

// src/components/photoGallery/canvas.ts
/**
 * 設置當前 EachSizeWidthArray 的索引,用於 放大縮小
 */
private setCurScaleIndex() {
  const cImgWidth = this.cImgWidth || this.image.width;

  const EachSizeWidthArray = this.EachSizeWidthArray;

  const curScaleIndex = getCurImgIndex(EachSizeWidthArray, cImgWidth);

  this.curScaleIndex = curScaleIndex;
}
  • getCurImgIndex

咱們經過此方法來獲得當前圖片款的索引值,他是根據當前渲染的圖片寬度,去 尺寸數組 取出最接近預覽圖片寬度,從而獲得當前圖片的 index,具體實現你們能夠參考源碼。

 

放大縮小邏輯

放大預覽的邏輯實際上就是根據放大以後的尺寸,計算出當前圖片的距離 canvas 頂部的高度 imgTop、以及距離左側 canvasimgLeft

前面咱們已經獲得首次圖片展現索引了,當咱們點擊放大的時候,無非就是將當前索引值加一,縮小就是減一。

咱們能夠根據新的索引值去 尺寸數組 中取出對應索引的寬度,經過圖片原始寬高,能夠等比例獲得當前應該顯示的寬高,最後咱們只須要計算出,放大後的圖片的 imgTopimgLeft 的值,其實就能實現功能了:

/**
 * 修改當前 圖片大小數組中的 索引
 * @param curSizeIndex :  
 */
public changeCurSizeIndex(curSizeIndex: number) {
  let curScaleIndex = curSizeIndex;

  if (curScaleIndex > 12) curScaleIndex = 12;
  if (curScaleIndex < 0) curScaleIndex = 0;

  // 畫布寬高,即顯示容器寬高
  const cWidth = this.cWidth;
  const cHeight = this.cHeight;

  // 上一次的索引
  const prevScaleTimes = this.curScaleIndex;
    // 尺寸數組
  const EachSizeWidthArray = this.EachSizeWidthArray;

  let scaleRadio = 1;

    // 這一次寬度與上一次的比值
  // 經過這個值能更方便的獲得圖片寬高
  scaleRadio = EachSizeWidthArray[curScaleIndex] / EachSizeWidthArray[prevScaleTimes];

  // 當前圖片寬高
  this.cImgHeight = this.cImgHeight * scaleRadio;
  this.cImgWidth = this.cImgWidth * scaleRadio;

  // 獲得最新的 imgTop
  // imgTop 值正負值是根據畫布左上角的點,向下爲正
  this.imgTop = cHeight / 2 - (cHeight / 2 - this.imgTop) * scaleRadio;
  // 設置當前 索引值
  this.curScaleIndex = curScaleIndex;

  // 若是圖片沒有超過畫布的寬和高
  if (this.cImgHeight < cHeight && this.cImgWidth < cWidth) {
    this.imgTop = (cHeight - this.cImgHeight) / 2;
  }

  // imgLeft 的計算
  this.imgLeft = cWidth / 2 - this.cImgWidth / 2;

  // 在圖片滑動的時候或者拖動的時候須要用到
  this.LongImgTop = this.imgTop;
  this.LongImgLeft = this.imgLeft;

  // 繪製圖片
  // ...
}

 

事件

滾動事件

canvas 中進行圖片滾動,其實就是從新計算圖片的 imgTopimgLeft,而後對其進行從新繪製。

這裏咱們使用滾輪事件 onWheel 來計算滾動的距離 ,經過事件對象 event 上的 deltaXdeltaY 獲得的在 x/y 軸上的滾動距離。

這裏須要注意的一個點是對邊界值的處理,imgTop 不能無止境的大和小,其最大不能超過咱們以前規定的 LONG_IMG_TOP 這個值,咱們設置的是 10px,最小能夠參照下面的計算方式(寬度的邊界值計算相似,就不作介紹了)

/**
 * minImgTop:最小的 imgTop 值
 * maxImgTop:最大的 imgTop 值
 * imgHeight:圖片高度
 * winHeight:屏幕高度
 * footerHeight:底部操做欄高度
 * LONG_IMG_TOP:咱們設置的一個上下常量 padding
 */
// 最小確定是負數
minImgTop = -(imgHeight - (winHeight - footerHeight - LONG_IMG_TOP))
// 最大
maxImgTop = LONG_IMG_TOP

接下來咱們在 canvas 類中定義一個 WheelUpdate 事例方法,暴露出去給外部調用,

// src/components/photoGallery/canvas.ts

/**
 * 滾輪事件
 * @param e wheel 的事件參數
 */
public WheelUpdate(e: any) {
    // ...

  // 圖片顯示容器的寬高
  const cWidth = this.cWidth;
  const cHeight = this.cHeight;

  // 若是圖片的寬高都小於圖片顯示容器的寬高就直接返回
  if (this.cImgHeight < cHeight && this.cImgWidth < cWidth) {
    return;
  }

  // 若是圖片的高度 大於 顯示容器的 高度
  // 則容許 在 Y 方向上 滑動
  if (this.cImgHeight > cHeight) {
    // 此值保存當前圖片距離容器 imgTop
    this.LongImgTop = this.LongImgTop - e.deltaY;

    // e.deltaY 向下
    if (e.deltaY > 0) {
      // 這裏作一個極限值的判斷
      // 具體是咱們的算法
      if ((-this.LongImgTop) > this.cImgHeight + LONG_IMG_TOP - window.innerHeight + this.footerHeight) {
        this.LongImgTop = -(this.cImgHeight + LONG_IMG_TOP - window.innerHeight + this.footerHeight);
      }
    } else {
      // 往上滑的時候,最大值是兼容值 LONG_IMG_TOP
      if (this.LongImgTop > LONG_IMG_TOP) {
        this.LongImgTop = LONG_IMG_TOP;
      }
    }
  }

  // 處理 x 軸上的滾動 
  // ...

  // 賦值 imgTop,imgLeft
  this.imgTop = this.LongImgTop;
  this.imgLeft = this.LongImgLeft;

  // 繪製圖片
  // ...
}

 

拖動事件

圖片拖動的咱們須要藉助 onMouseDownonMouseMoveonMouseUp 三個事件函數。其實操做方式可圖片滾動相似,咱們須要計算出新的 imgTopimgLeft 去從新繪製圖片,可是咱們不能經過 event 下面直接獲得拖動的值了,須要經過後一次與前一次的差值,來得出拖動的距離,進而計算出 imgTopimgLeft 值,

首先咱們把圖片拖動過程當中的實時座標掛在實例屬性 curPos 上,在 onMouseDown 的時候進行初始座標賦值,這樣在 onMouseMove 函數中咱們就能獲得鼠標按下的初始座標了。

// src/components/photoGallery/index.tsx

/**
 * 鼠標按下事件
 * @param e
 * @param instance : 圖片預覽的實例
 */
const MouseDown = (e: any, instance: any) => {
  // 全局 moveFlag 表示拖動是否開始
  moveFlag = true;
  const { clientX, clientY } = e;

  // 給當前預覽實例設置初始 x、y 座標
  instance.curPos.x = clientX;
  instance.curPos.y = clientY;

  // ...
};

/**
 * 鼠標擡起事件
 */
const MouseUp = (e: any) => {
  moveFlag = false;
};

/**
 * 鼠標移動事件
 */
const MouseMove = useCallback((e: any, instance: any) => {
  // 直接調用實例下的 MoveCanvas 方法
  instance.MoveCanvas(moveFlag, e);
}, [])

接下來咱們看一下最主要的拖動方法 MoveCanvas,咱們經過實時的座標值減去上一次的座標值(curPos 保存的值)作比較,得出滑動的距離,這樣咱們便能得出最新的 imgTopimgLeft 值了,固然這裏也不要忘記對邊界值的計算。

// src/components/photoGallery/canvas.ts

/**
 * 鼠標拖動的事件
 * @param moveFlag : 是否能移動的標誌位
 * @param e
 */
public MoveCanvas(moveFlag: boolean, e: any) {
  // 在拖動狀況下才執行拖動邏輯
  if (moveFlag) {
    // 圖片顯示容器的寬高
    const cWidth = this.cWidth;
    const cHeight = this.cHeight;
        
    if (this.cImgHeight < cHeight && this.cImgWidth < cWidth) {
      return;
    }

    // 當前滑動的座標
    const { clientX, clientY } = e;

    // 上一次座標
    const curX = this.curPos.x;
    const curY = this.curPos.y;

    // 處理 Y 軸上的滾動 
    if (this.cImgHeight > this.cHeight) {
      // 此值保存當前圖片距離容器 imgTop
      this.LongImgTop = this.LongImgTop + (clientY - this.curPos.y);
      // 與滾動相似的邊界值計算
    }

    // 處理 x 軸上的滾動 
    // ...

    // 更新實例屬性上的 x、y 值
    this.curPos.x = clientX;
    this.curPos.y = clientY;

    // 賦值 imgTop,imgLeft
    this.imgTop = this.LongImgTop;
    this.imgLeft = this.LongImgLeft;

        // 繪製圖片
    // ...
  }
}

 

預覽插件關閉

咱們在點擊圖片的時候去關閉圖片預覽插件,不過這裏須要考慮的是,咱們可以拖動圖片,當用戶是拖動圖片的時候,咱們就不須要關閉插件,因此咱們就須要判斷用戶鼠標按下以前和以後, x/y 座標值有沒有發生過改變,若是發生過改變了,那咱們就不執行關閉操做,不然直接將預覽插件直接關閉。

由於 mosueDownmouseUp 事件是要早於 click 事件的,咱們設置一個標誌位 DoClick,若是鼠標按下先後位置沒變的話,此標誌位就爲 true,那麼當圖片點擊的時候,就直接進行關閉,反之就不處理。

// src/components/photoGallery/index.tsx

const MouseDown = (e: any, instance: any) => {
  // ...
  StartPos.x = clientX;
  StartPos.y = clientY;
}

const MouseUp = (e: any) => {
  if (e.clientX === StartPos.x && e.clientY === StartPos.y) {
    DoClick = true;
  } else {
    DoClick = false;
  }
}

const Click = () => {
  if (!DoClick) return;
  
  const { hideModal } = props;
  if (hideModal) {
    hideModal();
  }
}

 

其餘知識點

圖片類什麼時候實例化

咱們以前建立了一個預覽圖片的類,那麼具體須要在何時去實例化呢?

只須要監聽在傳入的 imgUrl 變化的時候,就去把以前的實例清空,同時新實例化一個插件就 ok 了。

// src/components/photoGallery/components/Canvas.tsx

const Canvas = (props: Props): JSX.Element => {
  // ...
  // canvas 的 dom 元素
  let canvasRef: any = useRef();
  // 存放預覽圖片實例的變量
  let canvasInstance: any = useRef(null);

  useEffect((): void => {
    if (canvasInstance.current) canvasInstance.current = null;

    const canvasNode = canvasRef.current;

    canvasInstance.current = new ImgToCanvas(canvasNode, {
      imgUrl,
      winWidth,
      winHeight,
      canUseCanvas,
      // 圖片加載完成鉤子
      loadingComplete: function(instance) {
        props.setImgLoading(false);
        props.setCurSize(instance.curScaleIndex);
        props.setFixScreenSize(instance.fixScreenSize);
      },
    });
  }, [imgUrl]);
  
  // ...
}

有了這個圖片實例 canvasInstance,對於這張預覽圖的各類操做,好比 放大縮小 咱們均可以調用其擁有的方法就能夠簡單實現了。

 

屏幕尺寸

當咱們在屏幕尺寸變化的時候,須要根據最新的尺寸去實時繪製圖片,這裏咱們寫了一個自定義 Hooks,監聽屏幕 size 的變化。

// src/components/photoGallery/index.tsx

function useWinSize(){
  const [ size , setSize] = useState({
    width:  document.documentElement.clientWidth,
    height: document.documentElement.clientHeight
  });
  
  const onResize = useCallback(()=>{
    setSize({
      width: document.documentElement.clientWidth,
      height: document.documentElement.clientHeight,
    })
  }, []);

  useEffect(()=>{
    window.addEventListener('resize', onResize, false);

    return ()=>{
      window.removeEventListener('resize', onResize, false);
    }
  }, [])

  return size;
}

 

canvas 繪製閃爍

還有一個問題就在 canvas 繪製過程當中,當屏幕 resize 的過程當中會出現閃爍的問題,以下圖:

這是由於重繪畫布的時候,咱們須要使用 clearRect 來清空畫布,此時的畫布是空的,開始重繪須要相應的時間,所以在視覺會出現閃屏的狀況。解決閃屏,其實就是怎麼解決繪製時間較長的問題

咱們能夠參考 雙緩存 的概念來解決這個問題,將繪製過程交給了 緩存 canvas,這樣頁面中的 canvas 就省去了繪製過程,而 緩存 canvas 並無添加到頁面,因此咱們就看不到繪製過程,在 緩存 canvas 繪製好以後,直接將其賦給頁面原來的 canvas 這樣就解決了閃屏的問題。

// src/components/photoGallery/canvas.ts

class ImgToCanvas {
  // ...
  private cacheCanvas : any;
  private context : any;
  
  // ...
  
  private drawImg (type?: string) {
    // 頁面中 canvas
    const context = this.context;
    // ...
    
    // 建立一個 緩存 canvas,並掛到實例屬性 cacheCanvas 下
    if (!this.cacheCanvas) {
      this.cacheCanvas = document.createElement("canvas");
    }

    // 設置 緩存 canvas 的寬高
    this.cacheCanvas.width = this.cWidth;
    this.cacheCanvas.height = this.cHeight;
    // 建立畫布
    const tempCtx = this.cacheCanvas.getContext('2d')!;

    // 使用 緩存 canvas 畫圖
    tempCtx.drawImage(image, this.imgLeft, this.imgTop, this.cImgWidth, this.cImgHeight);

    // 清除畫布,並將緩存 canvas 賦給 頁面 canvas
    requestAnimationFrame(() => {
      this.clearLastCanvas(context);
      context.drawImage(this.cacheCanvas, 0, 0);
    })
    
    // ...
  }
}

 

小結

這篇文章整理了一個仿水墨圖片預覽插件從零到一的實現過程,從 思路分析代碼結構劃分主要邏輯的實現 這幾個方面闡述了一波。

經過這個插件的編寫,筆者對於 canvas 的畫圖 api、如何處理 canvas 繪圖過程當中出現的圖片閃爍的問題,以及對於 React Hooks 的一些用法有了大體的瞭解。

實不相瞞,想要個贊!

 

參考內容

相關文章
相關標籤/搜索