图片横向排版最佳实践

这篇文章也属于 VOID 主题开发过程的技术笔记,记录一下我的图片排版实现。

请注意,本文编写于 169 天前,最后修改于 48 天前,其中某些信息可能已经过时。

将图片排版引入到博客中是我自己的一个小需求。有时候想要分享一些平时的随手拍,但是直接在网页上一张张堆叠照片又显得并不美观,因此着手研究这个功能。

第一次的尝试实现是在 RAW 主题里,在这里可以看到效果。看似还行,但其实非常 naive,在许多情况下都会出现 bug 导致版式错乱。

目前我的最佳实现已经应用在了 VOID 主题中,在示例页面中可以看到效果。另外,VOID 主题用户中也有深度使用了这个功能的,例如这里,效果也足够令人满意,那么这篇文章就说说实现细节。

常见的图片排版方式

在网页上展示图片集合一般有几种方式:裁切为正方形、纵向瀑布流、横向排版。当然除了这三者,还有诸如 slider、popover 等展示方式,这些展示方式不在本文讨论之列。

裁切为正方形的展示方式很常见,微博、朋友圈等都是这样;Twitter 虽然有些许变化(不是所有的图片都等大),但是也在裁切为正方形的阵营里。背后的原因有产品上的考虑,也有技术上的考虑,在此不多说。但是若从个人分享摄影作品的角度来说裁切为正方形算不上是好的方案,因为总有一部分像素会丢失,除非看客有意点击展开,否则这部分内容就没有机会被展示了。况且某些照片有着精妙的构图,没人希望自己的构图被破坏。

纵向瀑布流也是很常见的展示方式,主要应用在专职展示图片的网站上,例如 PinterestUnsplash。这种展示方式的特点是列数一定,但是每列中的图片高度不一,图片能够保持自己的宽高比。这是不错的方案,特别是应用在无限滚动的网页上,不过也有一个相当大的缺点:不适合图文混排。图片列数一定,每列高度不同,自然使得最底下一行参差不齐,不适合作为文章中插入图片的方式。另外,这个方式有一个缺点:图片顺序不易保持,这是纵向瀑布流的通病,因此不论是内容块还是图片,纵向瀑布流都更适合顺序不那么重要的场景。

以上两种方式各自有自己的应用场景,但不是本文要重点讲的方式,因此其技术细节也就略过了(实际上也不难)。本文主要说图片的横向排版,效果如下(请在电脑端查看完整效果):

大运村鸟瞰
大运村鸟瞰
北航运动场
北航运动场

特点显而易见:图片排版以行为单位,每行中可以有任意张任意宽高比的图片,因此只需要处理一行中的图片排版问题,那么无论再来多少行效果都令人满意。应用这种排版方式的网站有 500px百度图片等。

实现细节

HTML 与 CSS

我们只关注一行图片。为了使文章具有足够的参考价值,我这里使用完整的 HTML 结构(这也是 VOID 使用的 HTML 结构):

<div class="photos">
    <figure>
        <div><img src="..." /></div>
        <figcaption>...</figcaption>
    </figure>
    ...    <!--若干个类似的 figure 结构-->
</div>

使用 figure 标签是为了使每张图片都有完整的展示与图题。照例,看 Demo:

从 HTML 结构中可以看出,div.photos 是一行图片的容器,每张图标使用 figure 包裹,并且在 img 标签外套上一层 div,这是为了模拟某些情况下的特殊需求(例如灯箱)。

最外层的容器 div.photos 设定为 flex 布局,方向为横向(flex-wrap: wrap 是为了在小屏幕下使用媒体查询使图片每张占一行,否则图片就会小得看不清了……)。

figure 标签除了一个 margin 属性用来控制间距和一个 position 属性来指定定位方式之外没有更多的内容。figure 下的 div 标签显式地指定了 height 为 0,并指定了 position 为 relative。这两个标签上的样式并不多,但是图片排版全看这两个标签的定位与尺寸,这部分放到后面。

首先不妨假设 div 标签与 figure 标签都有了合适的大小,那么 img 很好处理,指定为绝对定位,并且使之长宽等于父元素的长宽即可。现在就来看看如何使 div 标签与 figure 标签合理布局。

关键的部分

核心知识点有两个:padding-topflex-grow

padding-top 用来保持宽高比的 trick 想必很多人都知道。根据 MDN

当内边距(padding)是一个百分比的时候, 百分比是和包含块(containing block)的宽度有关的...

当 img 标签的直接父元素(即 div)得到了合理的宽度,然后通过 padding-top 属性来维持容器宽高比与图片宽高比相同。这样 div 本身 height 为 0,但是通过 padding-top 将它撑大,img 标签相对它绝对定位,尺寸与之相同,就是合理的结果。

最后只剩下一个关键的点:img 父元素即 div 的宽度如何确定。这个问题就是解决如何将一批长宽比不尽相同的图片塞到一行里并且保证底部平齐。

这里用到了 flex 布局的一个知识点:flex-grow。flex 布局中允许容器中的元素拉伸以填充整个容器的可用空间,其中每个子元素的拉伸比例即可通过 flex-grow 定义。例如:

div:nth-of-type(1) {flex-grow: 1;}
div:nth-of-type(2) {flex-grow: 3;}
div:nth-of-type(3) {flex-grow: 1;}

这段 CSS 使得第二个元素在拉伸时宽度总是别的元素的 3 倍。flex-grow 的值不重要,值之间的比值才重要。若能够在拉伸时保持各元素的比例,那么事情就变得简单了一些:只需要先把一行元素的高度都搞到相同,然后在横向按比例拉伸,那么问题便解决了。

示意图
示意图

先说实践方案,首先定义一个“基准值”(上面的 Demo 中是 50),然后计算把每张图片都缩到高度为基准值时图片的宽度。设图片原始宽度为 $w$ 与 $h$,基准值 $base=50$ ,则图片等比缩到 50px 时的宽度为:

$$ w'=\frac{w}{h}\times base$$

对每个容器如此处理,即可得到一行高度为 50px 且宽高比与图片相同的容器,这时再指定 flex-grow 使容器填充一行内的可用空间。这一步很巧妙,每个容器的 flex-grow 只需要设置为:

$$flex\_grow=w'=\frac{w}{h}\times base $$

也就是 flex-grow 与 $ w'$ 恰好相同即可。这是由于前面所述的,“flex-grow 的值不重要,值之间的比值才重要”。此时就完成了整个排版,至于如何获得图片的原始尺寸等属于细枝末节的问题,看 Demo 代码即可。


这篇文章也属于 VOID 主题开发过程的技术笔记。写主题好玩,写完了继续维护就不好玩了……希望到 2.0 版本的时候能够到达比较稳定的状态,然后就进入 LTS 阶段吧。

添加新评论

已有 14 条评论

是怎么把codepen嵌入到文章中的?

熊猫小A 熊猫小A 回复 @涛涛

CodePen 有嵌入代码

涛涛 涛涛 回复 @熊猫小A

好的,找到了

主页简化之后观感还不错

熊猫小A 熊猫小A 回复 @左岸

谢谢,我也觉得现在比较耐看。

大佬好,已经搬好板凳等开课了

隔壁萌新前来围观 (:大佬就是厉害.jpg

熊猫小A 熊猫小A 回复 @七越

我也是萌新

进入有bug就修,没bug就阶段

熊猫小A 熊猫小A 回复 @Mikusa

说对了 φ( ̄∇ ̄o)

大佬就是大佬

hades 大佬又来商业吹捧了 ╮(╯▽╰)╭

很棒,能不能把开发VOID的笔记放在一起,这样也好看

熊猫小A 熊猫小A 回复 @涛涛

可以在这个标签下看到,不过其实并没有太多值得说的内容。