教你如何用 CSS 实现一个可缩放的 DIV

4,079 阅读6分钟

前言

在和崽种群友聊天的时候,突然问到这么一个问题:如何实现 div 的缩放?

当即就让他自己去百度,网上此类代码太多了。

随之我们讨论到 <textarea> 标签,是自带缩放功能的,而且它的缩放好像没有直接的触发任何 JS 事件?

然后便有了2个问题

能不能不依赖 JS 实现一个可缩放的 div?

如何监听一个元素的缩放?

阅读本文,你能得到这两个问题的答案。

前言中的缩放并不严谨,多行文本编辑器自带的功能只是调整其容器大小,是 resize,而缩放指的是 scale,接下来我们会严格区分这两个概念。

认识 <textarea>

让我们线认识一下 HTML 的 <textarea> 元素,它表示一个多行纯文本编辑控件。

默认状态下,其右下角有短斜线,鼠标悬浮时会改变鼠标样式为 cursor: nwse-resize,可以点击拖动改变文本框的大小。

image.png

如果不想要这个短斜线或不允许改变文本框大小,可以通过设置 CSS 实现

textarea {
  resize: none;
}

改变 <div> 的大小

修改 textarea 的大小是通过 resize 属性实现的,我们也为 div 也加上 resize 属性。

然而,并没有生效?

仔细想想 div 和 textarea 有什么不同,好像是有个 overflow 的属性。其实如果你有过尝试的话,会发现修改 textarea 的 overflow 的属性是不生效的,始终为 scroll

现在我们知道,想要改变 div 大小,还需要一个条件:其 overflow 属性不为 visible

这样子,我们就能项文本框一样,改变 div 的大小。

div {
  width: 100px;
  height: 100px;
  background-color: aquamarine;
  resize: both;
  overflow: hidden;
}

image.png

缩放 <div>

通过修改 resizeoverflow 我们只是实现了改变容器的大小,如果想要其中的内容跟着缩放,内部的内容就需要根据比例设置宽高,方便期间,我们内部只放一张图片。

HTML

<div><img style="width: 100px; height: 100px" src="./1.jpg" /></div>
<div><img style="width: 100%; height: 100%" src="./1.jpg" /></div>

CSS

div {
  width: 100px;
  height: 100px;
  background-color: aquamarine;
  resize: both;
  overflow: hidden;
  margin-right: 20px;
  float: left;
}

image.png

锁定纵横比

缩放不应该是任意的,我不允许帅气的路哥变成这副鬼样子!

image.png

那么又是一个难题,如何锁定纵横比?

我们想到一个解决方案,能不能只缩放水平方向,同时让盒子在垂直方向上自适应,从而实现锁定纵横比。

然后想一想,如何实现垂直方向上的自适应?

既然一层盒子无法实现,我们可以嵌套两层盒子,在改变父盒子宽度的同时,改变子盒子的高度,进而撑开父盒子,实现锁定纵横比。

有哪一个垂直方向上的 CSS 属性是与父盒子宽度有关的呢?

我猜聪明的你一定能想到 padding,如果将 padding-bottompadding-top 设置为百分比,这个百分比是根据父盒子的宽度决定的!

写出来试试

HTML

<div class="outer">
  <div class="inner">
  </div>
</div>

CSS

.outer {
  width: 100px;
  background-color: aquamarine;
  resize: horizontal;
  overflow: hidden;
}
.inner {
  position: relative;
  width: 100%;
  padding-bottom: 100%;
}

image.png image.png

效果非常完美,不管如何缩放,都是一个正方形!

为什么要选择 padding-bottom,而不是 padding-top 或者 margin 呢?

因为我们不仅仅是造出这个盒子,还要往子盒子内部放其他元素呀。

选择 bottom 是因为我们希望容器中的 content 部分处于盒子顶部;如果使用 margin的话子盒子的高度为 0,也就无法正常地在其内部添加元素了。

image.png

接下来把帅气的陆哥放进去

<div class="outer">
  <div class="inner">
    <img style="width: 100%; height: 100%" src="./1.jpg" />
  </div>
</div>

image.png

欸?把盒子挤下来了。让我们为其设置一个绝对定位,就不会把父盒子撑开了。

<div class="outer">
  <div class="inner">
    <img style="width: 100%; height: 100%; position: absolute" src="./1.jpg" />
  </div>
</div>

image.png image.png

完美实现锁定纵横比的缩放~

新 CSS 属性 aspect-ratio

其实呢,刚刚费那么大劲地去锁定纵横比,但是 CSS 已经提供了这样一个属性 aspect-ratio,这个属性可以直接为容器设置纵横比

使用这个属性,就不再需要两层的盒子嵌套,下面的代码就能实现同样的效果。

HTML

<div class="container">
  <img style="width: 100%; height: 100%" src="./1.jpg" />
</div>

CSS

.container {
  width: 100px;
  background-color: aquamarine;
  resize: horizontal;
  overflow: hidden;
  aspect-ratio: 1 / 1; /* 锁定纵横比 */
}

但这个属性的兼容性并不好,只有最新版本的浏览器才能够支持。

image.png

元素尺寸监听器 ResizeObserver

盒子虽然造好了,但内部的元素想要跟着一起缩放,需要全部以百分比的形式写样式,这显然是非常不友好的,更别说还有很多 CSS 属性比如 border 没法使用百分比。

然后我们想到了相对长度单位 em,它是根据父元素的 font-size 属性计算的。

如果我们在缩放的同时改变父盒子的字体大小,子盒子中元素根据 em 设置宽度,自适应的问题就解决了。

很遗憾,CSS 已经到头了,想要实时修改父盒子的字体大小,只能依赖于 JS 了。

那么问题来了,我们在缩放盒子的时候,触发了哪些事件呢?

大家肯定都能想到:鼠标按下、拖动、松开。传统的使用 JS 实现修改盒子大小的方式,就是通过这3个事件实现的。

不过我们将采取另一种更为简单的方式:ResizeObserver

ResizeObserver 接口可以监听到元素的内容区域或边界框的改变。

在这里简单介绍一下用法,详情请看 ResizeObserverResizeObserverEntry

ResizeObserver() 是一个构造器,接受一个回调函数并返回一个 resizeObserver 对象。

resizeObserver 对象具有 observeunobserve 方法,用于开始/结束对 DOM 元素的监听。

传递给ResizeObserver() 构造器的回调函数会接受一个参数,是由 ResizeObserverEntry 对象组成的数组。在 ResizeObserverEntry 对象身上,包含着 DOM 元素的尺寸信息。

最终代码如下: HTML

<div class="container">
  <img style="width: 5em; height: 5em" src="./1.jpg" />
</div>

CSS

.container {
  width: 100px; /* 共5em */
  font-size: 20px;
  background-color: aquamarine;
  resize: horizontal;
  overflow: hidden;
  aspect-ratio: 1 / 1; /* 锁定纵横比 */
}
img {
  width: 5em;
  height: 5em;
}

JS

const resizeObserver = new ResizeObserver((entryList) => {
  // 获取元素与尺寸信息
  const { target, contentRect } = entryList[0]
  target.style.fontSize = contentRect.width / 5 + 'px'
})
const container = document.querySelector('.container')
resizeObserver.observe(container) // 开始监听

存在的问题

因为我们设置的是 resize: horizontal,所以鼠标指针悬浮的双箭头是水平方向的,与实际并不相符,浏览器没有暴露出设置这个角标样式的伪类元素,作者没有找到能够解决此问题的方法。

结语

如果文中有错误或不严谨的地方,请务必给予指正,十分感谢。

如果喜欢或者有所启发,欢迎点赞关注,鼓励一下新人作者。