給 dom 元素添加 onresize 功能

image

html 元素自適應

對於我們做前端可視化的人來說,最苦惱的一個地方莫過於,客戶需要我們對產品做自適應,特別是還需要做 pc 端的自適應。

一般,面對這個需求的時候,由普通的 html 元素(不包含 canvas)構成的頁面,你可以通過對元素的尺寸進行特殊的設置,不採用常用的 px 方案,而是通過設置百分比、vw、em 等方式,或者通過媒體查詢,或者通過近些年比較流行的 flex 彈性佈局 等等方案來解決這個問題。

這麼多方案,從中選一種,肯定會適合你的一款。

canvas 自適應的問題

但是對於 canvas 來說,以上方案就捉襟見肘了。

canvas 相當於一個畫布,我們朝 canvas 上面添加內容相當於是在畫布上繪圖。

因此,當 canvas 元素物理尺寸改變的時候,我們畫布上的內容,必須要清空了重畫。不然,我們繪製到其中的內容,就會被放縮,看起來就會失真了。

而且對於 canvas 來說,它本身有個 width 和 height 來控制繪圖區域的尺寸的,一般我們稱之爲 canvas 畫布尺寸。

參考頁面:https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement

image.png

一般情況下,這個畫布尺寸需要設置成和 canvas 元素的實際尺寸等大,這個尺寸也就是通過 canvas 元素的 css 屬性 height、width 來控制的。

不然,就會出現尺寸比較難控制,跑偏的情況(這個一般出現在 canvas 尺寸比其 css 控制的尺寸大的情況);或者會出現 canvas 上繪製的內容模糊的情況(一般出現在 canvas 尺寸比其 css 控制的尺寸小的情況)。

第一種情況

第二種情況

可以看到,這兩種情況的顯示效果都不太好。

一般的解決思路

因此爲了使得我們的 canvas 畫布的大小,與他自身物理尺寸的大小相互吻合,我們必須要在 canvas 物理尺寸發生變化的時候,做出一定的應對策略。

這就是所謂的事件監聽回調的機制。

但是問題就出現在,如果我們的 canvas 元素,是通過手動設置 px 尺寸,來控制大小的,那麼,我們可以順便在設置 px 的時候,順手把 canvas 畫布的大小改變了,這種邏輯用 js 實現起來很簡單。

比如用 window 對象上的 onresize 事件監聽窗口的變化,然後在 canvas 上套一個父 div 對象,父 div 對象 css 屬性的寬高設置成 100% 這種。然後,當窗口大小發生變化的時候,我們就能調用我們事先寫好的回調函數,裏面會獲取到父 div 的尺寸,設置到 canvas 上去,完成我們的自適應操作。

但是這個地方有個缺點是,如果我們界面上的佈局是可以手動變化的,比如有個側邊欄,可以展開收攏,那麼此時我們的 onresize 事件就失效了,我們必須要手動管理尺寸的變化,手動調用 onresize 的回調了。

這樣還是比較麻煩的。

有沒有一勞永逸的方法呢?canvas 自身尺寸變化的時候,爲什麼就沒有監聽事件呢?這難道是設計的 bug?

而且一般在實際使用的情況下,我們往往不想採取上面那樣做,回調來回調去的,太麻煩了。

有時候,我們有多個元素需要這種自適應處理,我們還得針對每個元素都進行這樣的處理,着實不好管理。

理想的解決方案

往往,我們想達到的理想的狀況是,我們能通過設置百分比或者 vw 這些方式來設置元素的尺寸。

那到底在元素尺寸變化的時候,有沒有辦法能監聽到變化,並且做出改變呢?

答案當然是有的,就藏在我們的 stackoverflow 上:https://stackoverflow.com/questions/10086693/resize-on-div-element

答案截圖

簡單的說,就是通過給元素,設置一個 iframe 子元素。

給 iframe 的寬高設置成 100%,那麼他就會跟隨着父元素來變化。

而且 iframe 又可以添加 onresize 監聽。

自適應小 demo

這個原理說起來簡單,但是一下子你還真不一定能想得到。

而下面是我優化後實現這個功能的關鍵性代碼:

function setResize(target, callback) {
  // 創建 iframe
  var iframe = document.createElement('iframe');
  // 改變樣式
  iframe.style.cssText = `
    position: absolute; left: 0; top: 0; width: 100%; height: 100%;
		border: 0; margin: 0; display: block; z-index: -999;
  `;

  // 將其設置爲傳入對象的孩子元素
  target.appendChild(iframe);

  var oldWidth = target.offsetWidth;
  var oldHeight = target.offsetHeight;

  // onresize 回調
  function resizeHandler() {
    var newWidth = target.offsetWidth;
    var newHeight = target.offsetHeight;
    if (oldWidth !== newWidth || oldHeight !== newHeight) {
      callback && callback({ width: newWidth, height: newHeight }, { width: oldWidth, height: oldHeight });
      oldWidth = newWidth;
      oldHeight = newHeight;
    }
  }

  var timer;
  (iframe.contentWindow || iframe).onresize = function() {
    /** 添加防抖機制 **/
    clearTimeout(timer);
    timer = setTimeout(resizeHandler, 20);
  };
}

當然以上代碼如果在實際中使用的話,還要考慮兼容性,還需要優化,但是這個功能基本的框架就是這樣的。

實際使用的時候 ,傳入 dom 對象,傳入回調函數:

let canvas = document.querySelector('canvas');
let parentDom = canvas.parentElement;
canvas.width = parentDom.clientWidth;
canvas.height = parentDom.clientHeight;

setResize(parentDom, (newData, oldData) => {
  canvas.width = newData.width;
  canvas.height = newData.height;
  render();
});

html 結構爲這樣:

<div id="root">
  <canvas></canvas>
</div>

css 樣式設置成這樣:

html,
body {
  margin: 0;
  padding: 0;
}
#root {
  width: 100vw;
  height: 100vh;
  position: relative;
}
canvas {
  width: 100%;
  height: 100%;
  display: block;
}

實際使用的過程中,拖動窗口的時候,效果如下:

拖動窗口時

因爲添加了防抖邏輯,所以在改變窗口大小的時候,變化稍微有點不太連續,但是實際情況下,也沒有人會進行連續變化的操作,所以防抖設置的還是合理的。

接下來,我們不變化窗口的大小,而是單獨改變父元素的尺寸,我們會發現,我們的策略同樣會生效。

單獨改變父元素尺寸
如果對這個 dom 感興趣,可以查看下在線示例:https://dist.coding.me/demo/dynamic%20update%20canvas/

後記

不得不說,這個方法纔是最完美的 Polyfill,至少我覺得是這樣的,不知道你看完以後覺得如何呢?

我不知道出於什麼考量,div 尺寸變化居然不能添加監聽。但是顯然,有時候,這個需求還是會存在的。

雖然這個方法也不完美,朝 dom 裏添加了多餘的元素。

但是我感覺,與 canvas 一起用,這個解決方法挺適合的,畢竟都用上 canvas 了,也不會在乎那一點性能損耗吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章