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
一般情况下,这个画布尺寸需要设置成和 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 了,也不会在乎那一点性能损耗吧。