容器长宽比

编辑推荐: 掘金是一个高质量的技术社区,从 CSS 到 Vue.js,性能优化到开源类库,让你不错过前端开发的每一个技术干货。 点击链接查看最新前端内容,或到各大应用市场搜索「 掘金」下载APP,技术干货尽在掌握中。

容器长宽比,这个话题在站上也有相关的文章介绍,最早出现于Responsive Web Design中,主要用来处理imgiframevideoobject这些元素的自适应问题。简单点讲,就是根据容器的宽度,按照宽高比例自动计算出容器的大小。并且让图片,视频之类能自适应容器。另外记得在知乎上有一个问题“移动端布局,div按比例布局,宽度为百分比,但又想让高度和宽度一样,即让div为正方形,怎么做布局呢?”,其实解决方案在前面的教程已提到过:

既然有相应的解决方案,继续花时间来说,是不是有点多余。那么这个问题又回到了CSS的根源上:在Web中,使用CSS解决问题,往往不只有一种方案,只有更适合的方案。

这两天看到@Chris Coyier特意也整理了一篇《Aspect Ratio Boxes》文章,里面有新的方案值得我们思考,特别是CSS自定义属性的部分。那我们再次花时间将相关方案整理在一起,仅供学习与参考。

方案一:基于宽度的百分比

首先介绍的方案是基于容器widthpadding一个百分比。这也是最早的一个方案。主要的原理是基于元素的padding-toppadding-bottom是根据元素的width进行计算的。假设你有一个div容器,它的宽度是500px,你想让其高度也是和宽度一样,也就是说宽高比例是1:1。这个时候借助padding-top或者padding-bottom的值为100%,就可以计算出容器div的高度是500px

这种方案有一个必要条件,容器divheight0,同时box-sizingborder-box,不然的话,容器不能带有border。现在我们可以想象一下,如果容器div的宽度又是一个百分比值,这样一来就可以保持容器高度跟宽度始一致。另外再想象一下,如果我们的padding-bottompadding-top不是100%,而是56.25%,其实这就是一个完美的宽高比16:9,也就是9 / 16 * 100% = 56.25%。如此一来,你可以根据你自己的设计比来进行调整。

而这样的场景仅适合容器中放置背景图片:

<div class="aspect-ratio-boxes"></div>

.aspect-ratio-boxes{
    overflow: hidden;
    height: 0;
    padding-top: 56.25%;
    background: url(/images/happy-birthday.svg);
}

注:如果背景图片不是SVG文件,那还需借助于background-size或者object-fit来处理。比如设置为cover这样的值。

回到我们的问题中来,很多时候,我们的图片比例并不是和容器比例一样,比如在这个示例中,有可能不是16:9。就算是一个SVG,假设这个SVG的viewBox = "0 0 1127.34 591.44",这也意味着它本质上是一个1127.34×591.44图像,它的比例是1127.34:591.44。或者说它也有可能是一个328×791的图形。

我想这种现象应该是很常见的,一个随机图像不一定符合预期的长宽比。那么问题就来了,对于一个任何长宽比,我们将如何实现?

任何可能的长宽比计算

对于固定的长宽比,比如16:9,它是完美的,我们也无需头痛。但事实上,往往并不如此,总是充满了随机性,这样也就表示比例也是随机性的。如果我们无法掌握随机性的比例,那么就会造成一个图像或视频被裁剪或者被拉伸(也有可能是被挤压)。这样对于追求完美的同学而言,是无法接受的。

那么需要自问一下,有没有办法通过CSS的计算,实现任何可能的长宽比计算呢?

值得庆幸的是可以的。在CSS中有一个神奇的calc()函数,它可以做一些基本的数学计算。拿上面的示例来说,SVG的长宽是1127.34×591.44,可以通过calc()计算出padding-top或者padding-bottom的值:

padding-top: calc(591.44 / 1127.34 * 100%);

这个时候可能有同学会提,使用calc()在客户端进行计算,会不会有性能问题?事实会不会呢?其实我回答不了,因为我没有进行过这方面的论证,同时也没有看到相关论证的文章。如果你对这方面感兴趣的话,不仿论证一下。除此之外,我的小伙伴说,任何性能问题随着硬件的发展都不会是问题。既然如此,我们忽略所谓的性能问题,回到这里。我们实际中使用calc()时,记得把单位带上,就上面的示例而言,我们的图片单位是px,此时在Sass这样的处理器中,可以直接这样计算:

padding-top: 591.44px / 1127.34px * 100%;

实际使用就是这么的简单。calc()都可以不使用了。

如果你对CSS有所了解的话,上面这样使用,很有可能是仅仅适合用于背景图像,如果我们的容器用不了背景图像。也就是说,容器里放置的是一个img或者说是一个iframevideo。那就会产生一个新问题。

如果有内容,那么容器设置了padding-top会把容器的内容往下推;如果设置的是padding-bottom,会把内容往上推。

接下来我们需要解决的就是这个问题。

如何在设置了padding的时候不把内容往下(上)推

就上面的示例而言,如果在div容器设置了padding-top(或者padding-bottom)会造成容器的内容往盒子外推,此时设置了overflow:hidden时,溢出的内容就会看不见了。为了解决这个问题,首先会想到的是position:absolute

假设我们有这样的一个示例:

<div class="aspect-ratio-box">
    <iframe class="aspect-ratio-box-inside" src="https://www.youtube.com/embed/upPCohrJcbw?showinfo=0&modestbranding=1" frameborder="0" allowfullscreen></iframe>
</div>

使用对应的CSS:

.aspect-ratio-box {
    height: 0;
    overflow: hidden;
    padding-top: 591.44px / 1127.34px * 100%;
    background: white;
    position: relative;
}
.aspect-ratio-box-inside {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
}

这是对于imgvideoiframe或者object的一个较好的解决方案。但如果我们容器.aspect-ratio-box-inside并不是这些元素,而是一些文本内容呢?我想你肯定会想到,将会出现什么问题?

很显然溢出容器的内容被裁切掉了。这个时候较好的解决方案就是把overflow:hidden换成overflow:auto。但也是美中不足,会出现滚动条。既然如此,有没有较好的解决方案呢?先不回答,咱们先来尝试下面这样的一种方案:借助CSS的伪元素来做容器的宽高比例

.aspect-ratio-box {
    background: white;
}
.aspect-ratio-box::before {
    content: "";
    width: 1px;
    margin-left: -1px;
    float: left;
    height: 0;
    padding-top: calc(591.44px / 1127.34px * 100%);
}
.aspect-ratio-box::after { /* to clear float */
    content: "";
    display: table;
    clear: both;
}

来看一个示例效果吧:

从效果中可以告诉我们,这样处理方式,如果内容不超出容器的时候,容器的大小还具有对应的宽高比,如果内容超出容器的时候,会扩展容器的高度,让内容能足已展示。这样处理是不是更完美一些。

事实上,前面介绍的这些方法,我们在以前的文章中都有介绍过。但这篇文章具有营养之处是下一节内容。前面花这么多篇幅主要是让大家对宽高比具有一个更形象的理解。那就进入下一节吧。

使用CSS自定义属性

至于什么是CSS自定义属性,这里不做过多的介绍,如果从示接触这方面的内容,可以点击这里进行了解。接下来的内容,我假装你了解了CSS的自定义属性。首先在:root里声明一个全局变量:

:root {
    --aspect-ratio: 1 / 1;
}

在实际使用的时候,可以借助CSS自定义属性的局部变量来覆盖全局变量。比如:

<div style="--aspect-ratio:815/419;">
</div>

<div style="--aspect-ratio:16:9;">
</div>

<!-- even single value -->
<div style="--aspect-ratio:1.4;">
</div>

CSS的样式可以这样写:

[style*="--aspect-ratio"] > :first-child {
    width: 100%;
}
[style*="--aspect-ratio"] > * {  
    height: auto;
} 
@supports (--custom:property) {
    [style*="--aspect-ratio"] {
        position: relative;
    }
    [style*="--aspect-ratio"]::before {
        content: "";
        display: block;
        padding-bottom: calc(100% / (var(--aspect-ratio)));
    }  
    [style*="--aspect-ratio"] > :first-child {
        position: absolute;
        top: 0;
        left: 0;
        height: 100%;
    }  
}

这个时候,如果借助vw这样的单位,在容器上做一定的处理:

[style*="--aspect-ratio"]{
    width: 50vw;
    margin: 20px auto;
    background:orange;
}

你将看到一个完美的效果:

改变浏览器视窗大小,你可以看到如下的一个效果:

想出这样的CSS处理方式的人是不是天才呀。这样的思路值得我们去思考。上面的示例演示的是iframe的方式,事实上,上面的方式适合imgvideo之类的。如果回到前面的示例,如果是文本内容呢?我想我不多说,你也能找出类似的解决方案。当然,你可能会好奇上面的CSS代码是什么意思?对于CSS的老司机而言,上面的代码不是问题,对于新同学而言,你只要理解了CSS的属性选择器、CSS的伪元素、CSS自定义属性以及CSS的calc()@supports就可以很好的理解了。

其他的解决方案和思路

CSS自定义属性的方案已经让我们感觉眼前一亮了。但除了上面介绍的一些方案,还有其他的解决方案,比如CSSplus,他就提供了一个Aspecty特性,可以在你的代码中直接使用:

div {
    background: lime;
    --aspect-ratio: 478/239;
}

这种方法需要引入一个JS文件。这是其中不足之处。除此之外,@sgomes写了一个CSS-aspect-ratio的CSS文件。你只需要将这个文件引入到你的项目中,你也可以很好的处理宽高比。比如

npm

npm i --save-dev css-aspect-ratio

或者在你的CSS文件中:

@import https://unpkg.com/css-aspect-ratio@1/css-aspect-ratio.css;

当然,你也可以将这个文件保存到你的本地,更疯狂的是,直接把这里面的代码Copy到你的CSS中。有了这个前提,你在项目中这样使用即可:

<div class="aspect-ratio"
style="width: 768px; --aspect-ratio-w: 4; --aspect-ratio-h: 3;">
    <img src="kitten.jpg" alt="A cute kitten">
</div>

另外还要一个就是PostCSS的插件:PostCSS Aspect Ratio。如果你的构建工具中已经使用了PostCSS,通过下面的命令就可以将这个插件安装到你的构建工具中:

npm install postcss-aspect-ratio --save

比如你要使用的宽高比例是16:9,那可以这样使用:

HTML

<div class="aspect-box">
    <div class="aspect-box__content">
        <!-- Any content you like, very useful for video and image elements. -->
    </div>
</div>

CSS

/* Input. */
.aspect-box {
    position: relative;
    background: lime;
    aspect-ratio: '16:9';
}

/* Output. */
.aspect-box {
    position: relative;
    background: lime;
    box-sizing: border-box;
}

.aspect-box > * /* This targets .aspect-box__content */ {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0; 
    left: 0; 
    box-sizing: border-box;
}

.aspect-box:before /* This pseudo element uses the padding trick to set the height. */ {
    position: relative;
    display: block;
    content: "";
    padding-top: 56.25%;
    box-sizing: border-box;
}

如果你想在一定的比例上稍做调整,比如在4:3的比例上,容器高度少个20px,可以这样使用:

/* Input. */
.aspect-box {
    position: relative;
    background: lime;
    aspect-ratio: calc('4:3' - 20px);
}

/* Output. */
.aspect-box {
    position: relative;
    background: lime;
    box-sizing: border-box;
}

.aspect-box > * {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0; 
    left: 0; 
    box-sizing: border-box;
}

.aspect-box:before {
    position: relative;
    display: block;
    content: "";
    padding-top: calc(75% - 20px);
    box-sizing: border-box;
}

是不是感觉很NB。如果你使用了,你也就变得NB了。不仿试试看。

总结

这是我见过,也是实战过的容器宽高比例的实现方案。不管是哪种方案,其基本原理都是不变的,和最原始的方案一样,借助于padding-top或者padding-bottom来实现。最简单的原理,容器的padding-top(或padding-bottom)的百分比是根据容器的width来计算的。唯有不同之处是,实现的手段不一样,那是因为我们CSS技术发展的变化。或者说开发者特别聪明,除了原生的开发,还可以借助一些工具,比如说JS插件,PostCSS插件之类的。最后再提一下,如果我们容器自身是一个百分比单位,或者说是一个视窗单位,比如示例中用的vw。那么我们就能更好的实现一些自适应的布局。特别是在响应式设计当中,就一两年前,要实现这样的效果还是很痛苦的。

最近也在思考一种更适合移动端的布局方案,其中思路就是借助于这篇文章的相关知识,目前正在实测阶段,如果通过测试之后,我们就可以告别以前所使用的REM方案(也就是《使用Flexible实现手淘H5页面的终端适配》分享的方案)。如果你感兴趣,欢迎持续关注相关更新。如果你对宽高比有其他的经验,欢迎与我们一起分享。

参考文档

大漠

常用昵称“大漠”,W3CPlus创始人,目前就职于手淘。对HTML5、CSS3和Sass等前端脚本语言有非常深入的认识和丰富的实践经验,尤其专注对CSS3的研究,是国内最早研究和使用CSS3技术的一批人。CSS3、Sass和Drupal中国布道者。2014年出版《图解CSS3:核心技术与案例实战》。

如需转载,烦请注明出处:https://www.w3cplus.com/css/aspect-ratio-boxes.html

返回顶部