-
Notifications
You must be signed in to change notification settings - Fork 102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
使用纯 CSS 实现 Google Photos 照片列表布局 #4
Comments
@cssmagic 魔法哥,快来看呀 |
上班时偷偷扫了一遍。前半段看得挺开心的,从中间开始一时没跟上。等有空再细看。 😃 |
@cssmagic 中间哪里没跟上?我看看是不是写的不清楚,我重写一下~ |
nice。。。。 |
JS bin的例子看不了 |
@woshija jsbin里的例子都引用了google cdn上的文件,所以应该需要翻墙才能看 |
我用JS来设置flex-grow,flex-basis,padding-bottom效果不行,这些参数是需要在服务端预先设置好,而不能在客户端等图片加载完再设置吗? |
|
Coool ~ CSS 也可以实现 👍 |
amazing! |
nice |
特别赞!从某论坛点链接点过来的,仔细看了一遍,准备star,发现以前已经star过这个博客系列了。解决问题的思路描述得很清晰,尤其是一些利用技术细节取巧特别亮,加油! |
good |
|
@hkongm 哈哈,赶快继续看,后面还有更好玩的~~ |
还是更喜欢最终 Demo http://output.jsbin.com/tisaluy/1 因为每行高度是一致的,看起来更加整齐。而修改版可能会有很高的行。 相比起修改版,最终版不需要服务器端或者浏览器端计算图片高度,是真正的纯 CSS 实现。 |
这个版本所有图片高度是一样的,所以必然会被裁剪。 |
这个其实有视觉上完美的解决方案,但是需要更多的空元素(占位符),后来想到后没有更新上来。
楼上也有同学提到类似你说的这个方案~
在 2017年1月23日 上午12:46,Guo Yunhe <[email protected]>写道:
… 一个补充。最后一行总会留有空白,有时这个空白非常小,就看着不太舒服(强迫症)。但可以通过限定伪元素的最小宽度 200px,让最后一行的空白小于
200px 时则通过裁切图片让最后一行完整填满。
section::after {
content: '';
flex-grow: 999999999;
min-width: 200px;
height: 0;
}
[image: screenshot_20170122_184354]
<https://cloud.githubusercontent.com/assets/5836790/22184026/fd71c1d8-e0d2-11e6-9b7f-69491f3d8113.png>
[image: screenshot_20170122_184429]
<https://cloud.githubusercontent.com/assets/5836790/22184027/fd727506-e0d2-11e6-9340-952ee1555e98.png>
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#4 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AC2vG_kgT7PKybF8_tiYUfaYGoRatSLTks5rU4flgaJpZM4INdx7>
.
|
demo里面flex-basis和flex-grow还是计算出来的么? |
能否提供最终完整的代码?十分感谢。 |
jsbin里引用的angular被墙了,看不到效果。 |
厉害 |
謝謝,受益良多 |
感謝大佬, |
怎么添加一个a标签....貌似做不到....只能通过js |
大佬666 不过大佬用了angular。并且是提前知道图片的宽高。https://jsbin.com/buqanohodu/edit?html,css,js,output |
这个html 有github 地址么?另外,支持ajax 瀑布流分页么? |
找了好多实现类似功能的方案,就你写得最详尽、最简洁且最符合需求,让人豁然开朗。另外,你知道google photos右侧的时间轴怎么做的么,对此我根本没有任何思路,望能赐教。 |
使用纯 CSS 实现 Google Photos 照片列表布局
文章太长,因为介绍了如何一步一步进化到最后接近完美的效果的,不想读的同学可以直接跳到最后一个大标题之后看代码、demo及原理就好,或者也可以直接看下面这个链接的源代码。
不过还是建议顺序读下去,因为后面的原理需要前面的内容做为铺垫,主要是在处理边角问题上。
先看下效果吧,要不然各位可能没动力读下去了,实在是有点长,可以试着 resize 或者 zoom 一下看看效果: https://xieranmaya.github.io/images/cats/cats.html
另外后面一些 demo 为了方便展示源代码用了 jsbin,但 jsbin 偶尔抽风会显示不出效果,试着在源代码编辑框里不改变代码意思的情况下编辑一下(比如在最后打一下回车)应该就可以了,或者查看一下你浏览器的翻墙设置,因为里面引入了 Google CDN 上的文件,有可能是因为 js 加载不成功导致的。
好了,正文开始。
开始之前,先对比一下三种比较常见的图片布局的差异
以上介绍的前两种布局都有一个共同点,那就是图片没有经过非等比拉伸,也就是说图片里的内容没有变形,也没有被裁剪,只是放大或者缩小,这是目前图片类应用在展示图片上的一个趋势,应该说,很少有专做图片的网站会把照片非等比拉伸显示(变形拉伸真的给人一种杀马特的感觉。。。),最次的也就是把图片等比拉伸后展示在一个正方形的区域里,类似于正方形容器的 background-size: cover; 的效果。
另外,在花瓣的布局中,比较宽的图片展示区域会比较小;而在第二种布局中,则是比较高的图片展示区域会比较小。
但是,在第一种布局中,因为宽度是定死了的,所以高宽比小到一定程度的图片,显示区域会非常小。而在第二种布局中,因为不同行的高度是不一样的,如果比较高的图片出现在比较高的行,还是有可能展示的稍大些的。
总体来说,以 Google Photos 为代表的图片布局,在显示效果上更优。关于如何使用 JS 来完成 Google Photos / 500px 布局的算法,这里就不讨论了,读者可以自己思考一下~
下面根据上面的分析稍微总结一下评判图片布局优劣的一些标准:
第一次看到类似 Google Photos 照片列表的布局已经不记得是在哪里了,当时只是觉得这种布局肯定需要 JS 参与,因为每行图片高度相同的情况下不可能那么恰到好处的在容器两端对齐,且所有图片之间的间距大小也一样(如果间距大小不一样但两端对齐,可以使用 inline 的图片加上 text-justify 来实现,在图片较小的时候(比如搜索引擎的图片结果)也不失为一种选择),通过观察,发现每行的高度并不相同,就确认了必然需要 JS 参与才能完成那样的布局。
然而当越来越多的开始网站使用这样的布局时,做为一个热衷于能用 CSS 实现就不用 JS 的前端工程师,我就在考虑,能否仅用 CSS 实现这样的布局呢,尤其是不要在 resize 时重新计算布局?
在经过一些尝试后,我发现可在一定程度上用纯 CSS 实现类似的布局,这里说的一定程度上仅使用 CSS 实现布局,我的意思是,布局一但渲染完成,布局后序的 resize,zoom 都可以在没有 JS 参与的情况下保持稳定,也就是说,首次的渲染甚至可以通过服务器完成,整个过程可以没有 JS 参与,所以说是用纯 CSS 实现也不过分。
实现过程
下面就来介绍一下我是如何只通过 CSS 一步一步实现的这个布局的
一开始,我们将图片设置为相同的高度:
这样并不能让图片在水平方向上占满窗口,于是我想到了 flex-grow 这个属性,让 img 元素在水平方向变大占满容器,整个布局也变成了 flex 的了:
把 flex container 的 flex-wrap 设置为 wrap,这样一行放不下时会自动折行,每行的图片图片因为 grow 的关系会在水平方向上占满屏幕,效果看上去已经很接近我们想要的了,但每张图片都会有不同程度的非等比拉伸,图片的内容会变形,这个好办,可以用
object-fit: cover;
来解决,但这么一来图片又会被裁剪一部分。最终 demo: http://jsbin.com/tisaluy/1/edit?html,css,js,output
不过上述的 DOM 结构显然是没办法在实际中使用的:
所以我们上面的这个布局事实上是没办法用于任何生产环境的。
接下来我们把 DOM 结构改成下面这样的:
我们为图片增加了一个容器。依然把图片设置为定高,如此一来,每个 div 将被图片撑大,这时如果我们给 div 设置一个 flex-grow: 1; ,每个 div 将平分每行剩余的空间,div 会变宽,于是图片宽度并没有占满 div,如果我们将 img 的 width 设置为 100% 的话,在 IE 和 FF 下,div 已经 grow 的空间将不会重新分配(我觉得这是个很有意思的现象,图片先把 div 撑大,div grow 之后又把图片拉大),但在 Chrome 下,为 img 设置了 width: 100%; 之后,grow 的空间将被重新分配(我并没有深究具体是如何重新分配的),会让每个容器的宽度更加接近,这并不是我们想要的。试了几种样式组合后,我发现把 img 标签的 min-width 和 max-width 都设置为 100% 的话,在 Chrome 下的显示效果就跟 IE 和 FF 一样了。最后我们将 img 的 object-fit 属性设置为 cover,图片就被等比拉伸并占满容器了,不过与前一种布局一样,每行的高度是一样的,另外图片只显示了一部分,上下两边都被裁剪掉了一些。
上面布局完整的 demo: http://jsbin.com/tisaluy/2/edit?html,css,output
在这种布局下,如果图片高度设置的比较小,布局已经没有什么大碍,因为图片越小就意味着每行图片越多而且剩余的空间越小并且剩余空间被更多的图片瓜分,那每个容器的宽高比就越接近图片的真实宽高比,多数图片都能显示出其主要部分。
唯一的问题是最后一行,当最后一行图片太少的时候,比如只有一张,因为 grow 的关系,它将占满一整行,而高度又只有我们设置的 200px,这时图片被展示出来的部分可能是非常少的,更不用说如果图片本身上比较高,而展示区域又非常宽的情况了。
针对这种情况,我们可以让列表最后的几张图片不 grow,这样就不至于出现太大的变形,我们可以算出每行的平均图片数量,然后用
然后配合 media query,在屏幕不同宽度时,让"最后一行"的元素个数在窗口宽度变化时也动态变化:
上面的代码写起来是相当麻烦的,因为每个屏幕宽度范围内又要写多个 nth-last-child 选择器,虽然我们可以用预处理器来循环迭代出这些代码,但最终生成出来的代码还是有不少重复。
有没有办法只指定最后多少个元素就行了,而不是写若干个 nth-last-child 选择器呢?其实办法也是有的,想必大家应该还记得 CSS 的
~
操作符吧,a ~ b
将选择在a
后面且跟a
同辈的所有匹配b
的元素,于是我们可以这么写:先选中倒数第 8 个元素,然后选中倒数第 8 个元素后面的所有同辈结点,这样,就选中了最后的 8 个元素,进一步,我们可以直接将选择器改写为
div:nth-last-child(9) ~ div
,就可以只用一个选择器选择最后的 8 个元素了。上面的几种选择尾部一些元素的不同选择器,实际上效果是不太一样的:
div:nth-last-child
的选择方法能保证倒数的 n 张图片一定被选中div:nth-last-child(n), div:nth-last-child(n) ~ div
只能保证当 div 元素至少有 n 个时才能选中最后的 n 个元素,因为如果:nth-last-child(n)
不存在,这个选择器就无效了div:nth-last-child(n+1) ~ div
则需要保证 div 元素至少有 n+1 个时才能选中最后的 n 个元素,原理同上选择最后若干张图片这种方式还是不够完美,因为你无法确定你选择的 flex item 一定在最后一行,万一最后一行只有一张图片呢,这时倒数第二行的前几张图片就会 grow 的很厉害(因为后面几张不 grow),或者最后两行图片的数量都没有这么多张,那倒数第二行就没有元素 grow 了,就占不满这一行了,布局就会错乱。
那么有没有办法只让最后一行的元素不 grow 呢?一开始我也了很多,甚至在想有没有一个 :last-line 伪类什么的(因为有个 :first-line),始终没有找到能让最后一行不 grow 的方法,然而最后竟然在搜索一个其它话题时找到了办法:
那就是在最后一个元素的后面再加一个元素,让其 flex-grow 为一个非常大的值比如说 999999999,这样最后一行的剩余空间就基本全被这一个元素的 grow 占掉了,其它元素相当于没有 grow,更进一步,我们可以用伪元素来做这件事(不过 IE 浏览器的伪元素是不支持 flex 属性的,所以还是得用一个真实的元素做 placeholder):
到这里,我们基本解决这个布局遇到的所有问题。
Demo,resize 或者 zoom 然后观察最后一行的图片:http://jsbin.com/tisaluy/3/edit?html,css,output
但还有最后一个问题,同前一种布局一样,如果你在线上去加载使用这种方式布局的网页,你会发现页面闪动非常厉害,因为图片在下载之前是不知道宽高的,我们并不能指望图片加载完成后让它把容器撑大,用户会被闪瞎眼。其实真正被闪瞎的可能是我们自己,毕竟开发时要刷新一万零八百遍。
所以,我们必须预先渲染出图片的展示区域(实际上几乎所有图片类网站都是这么做的),所以这里还是要小用一些 js,这些工作也可以在服务器端做,或者是用任何一个模板引擎(下面的代码使用了 angular 的模板语法)。
这个布局一旦吐出来,后续对页面所有的动作(resize,zoom)都不会使布局错乱,同时也不需要 JS 参与,符合前文所说的用纯 CSS 实现:
到这里,我们才算实现了图片的非等宽布局。
Demo,注意 html 模板里计算宽度的表达式:http://jsbin.com/tisaluy/4/edit?html,css,output
那么这个布局的展示效果究竟如何呢?
实际上我专门写了代码计算每张图片被展示出来的比例到底有多少:在图片高度为 150px 左右时,约有三分之一的图片展示比例在 99% 以上。最差的图片展示比例一般在 70% 左右浮动,平均每张图片展示比例在 90% 以上。图片越矮,展示效果会越好;图片越高,展示效果就越差。
因为这种方案最后也被我抛弃了,所以就不放计算展示比例的 demo 了。
看到这里,你应该是觉得被坑了,因为这并没有实现标题中说的 Google Photos 照片列表的布局
因为每行的高度是一样的,就必然导致大部分图片没有完全展示,跟 Google Photos / 500px 那些高大上的布局根本就不一样!
可是正文从现在才正式开始,下面介绍的方式也是我在实现了上面的布局后很久才想出来的,前面的内容只是介绍一些解决边角问题用的。
可以看到,前面的实现方式并没有让每张图片的内容全部都显示出来,因为每行的高度是一样的,而想要实现 500px 的布局,每行图片的高度很多时候是不一样的。
一开始我觉得,CSS 也就只能实现到这种程度了吧,直到我遇到了另一个需求:
我想用一个正方形的容器展示内容,并且希望无论浏览器窗口多宽,这些正方形的容器总是能铺满窗口的水平宽度而不留多余的空间(除了元素之间的空白),乍一看这个需求可能需要 JS 参与:读出当前浏览器窗口的宽度,然后计算正方形容器的 size,然后渲染。
可以看这个 demo,试着拉动一下窗口宽度然后看效果:
http://jsbin.com/tomipun/4/edit?html,css,output
拉动过程中可以看到,正方形的容器会实时变大,大到一定程度后又变小让每行多出一个正方形容器。 如果只看这一个 demo,可能各位不一定能一下子想到如何实现的,但如果只有一个正方形容器,它的边长总是浏览器宽度的一半,想必很多人都知道的,长宽比固定的容器要怎么实现吧?
我们知道(事实上很多人都不确定,所以这可以做为一个面试题),margin 和 padding 的值如果取为百分比的话,这个百分比是相对于父元素的宽度的,也就是说,如果我给一个 block 元素设置 padding-bottom(当然,也完全可以是 padding-top,甚至可以两个一起用~)为 100% 的话,元素本身高度指定为 0,那么这个元素将始终是一个正方形,并且会随着容器宽度的变化而变化,想要改变正方形的大小,只需要改变父容器的宽度就可以了:
看这个的 demo:
http://jsbin.com/lixece/1/edit?html,css,output
拉动窗口可以看到色块会变大,但始终保持正方形。当然,如果参照物是浏览器窗口,那么在现代浏览中,这个效果可以用 vw / vh 实现;但如果参照物不是浏览器窗口,就只能用垂直 padding 来实现了。
于是我就想到,如果不给 flex item 的元素设置高度,而是让其被一个子元素撑开,并且这个子元素的宽度是100%,padding-bottom 也是 100%,那么 flex item 及这个用来撑大父元素的子元素就会同时保持为正方形了,于是就实现了上面的那种正方形阵列布局。
但仅仅这样还不够,最后一行又会出问题,如果最后一行的元素个数跟前面的行不一样的话,它们虽然会保持正方形,但是因为 grow 的关系,会比较大,那如何保证最后一行的元素也跟前面的行大小相同呢,这时使用一个元素并设置很大的 flex-grow 让其占满最后一行剩余空间的做法已经不可行了,因为我们需要让最后一行的元素恰到好处的跟前面行的元素 grow 时多出一样的空间。
其实解决方案也很简单,把最后一行不当最后一行就行了!此话怎讲呢?
在最后添加多个占位符,保证可见的最后一个元素永远处于视觉上的最后一行,而让占位符占据真正的最后一行,然后把这些占位符的高度设置为 0 。具体添加多少个占位符呢?显然是一行最多能显示多少个元素,就添加多少个了,比如前面的 demo 就添加了 8 个占位符,你可以在源代码里面看一下。另外为了更好的语义,其实可以用其它的标签当做占位符。
这样一来,始终能占满水平宽度的正方形阵列布局也实现了。
本来我以为,到这里就结束了,即使用上最先进的 flexbox 布局,CSS 也无法实现图片不裁减的完美布局。
- FAKE EOF -
4 月 2 号的早上我醒来的时候,突然想到,既然可以让一个容器始终保持正方形,那岂不是也可以让这个容器始终保持任何比例?显然是可以的,只要我们把用于撑大父元素的那个元素的 padding-bottom 设置为一个我们想要的值就可以了!这样一来,说不定可以实现图片布局中,所有图片都完全展示且占满水平宽度的布局(也就是 Google Photos / 500px 的布局)!
当然,前面提到过,由于图片加载缓慢,图片布局往往都会提前知道图片的宽高来进行容器的预渲染,然后图片加载完成后直接放进去。
所以这里我们仍然需要用 JS 或者服务器来计算一下图片的宽高比例,然后设置到 padding-bottom 上面去,以保证容器的宽高比始终是其内部图片的宽高比。
我们先让所有图片以 200px 的高度展示,写出如下模板代码:
在上面布局中,因为 flex-wrap 的关系,每一行不够放的时候后面的内容就会折行,并且留出一些空白,每个容器的宽高比都是跟未来放入其内部的图片的宽高比是一样的,为了便于展示,我将图片大小设置为容器大小的四分之一,应该明显可以看出图片的右下角处于容器的中心位置。
Demo: http://jsbin.com/tisaluy/5/edit?html,css,output
下一步,我们只需要让所有的元素 grow 就可以了,那么是把所有的元素的 flex-grow 设置为 1 吗?
实际上如果设置了并看了效果,我们会发现并不是,因为我们希望每行元素在 grow 的时候,保持原有比例且高度相同。
Demo:http://jsbin.com/tisaluy/6/edit?html,css,output
可以看到如果给所有的 flex item 设置 flex-grow: 1; 的话,容器跟图片的比例并不一致,这里我将图片宽度设置了为容器的宽度以便观察。
通过一些简单的计算我们会发现,每行图片高度一致的时候,每张图片在水平方向上占用的宽度正好是其宽度在这一行所有图片宽度之和中所占的比例。
在前面不 grow 的情况下,每张图片的容器的宽度已经是按比例分配了,而每行的剩余空间,我们希望它仍然按照目前容器宽度所占的比例来分配,于是,每个容器的 grow 的值,正好就是它的宽度,只不过不要 px 这个单位。
最终的代码如下:
这样一来,容器会占满当前行,并且保持与未来内部所放入的图片相同的宽高比:
Demo: http://jsbin.com/tisaluy/8/edit?html,css,output
至于最后一行怎么处理,前面已经介绍过了,用一个 flex-grow 极大的元素占满剩余空间就可以了。
这种布局在渲染完成后,你可以放心的 resize 和 zoom,布局都不会错乱,而且没有 JS 的参与。
到这里,我们终于实现了类似 Google Photos / 500px 网站的图片布局。
总结一下这个方案的原理:
这种布局的优点:
最后说一下这种方案的一些缺点:
关于降级
由于 IE 9 都是不支持 flexbox 的,所以这个方案必然需要优雅降级,在不支持的浏览器上,让图片都以正方形展示应该也不会太差,然后用 float 或者 inline-block 来折行,这里就不细说了。
最后,多个这种布局 float 一下,就可以实现 Google Photos 那种某几个连续日期的图片太少时,展示在同一行的效果。读者可以自行试一下,就不放 demo 了。
本文到此结束,谢谢围观!文中如有纰漏之处,还请各位大神留言指正
The text was updated successfully, but these errors were encountered: