文章很長,因為介紹了如何一步一步進化到最后接近完美的效果的,不想讀的同學可以直接跳到最后一個大標題之后看代碼、demo 及原理就好,或者也可以直接看下面這個鏈接的源代碼。不過還是建議順序讀下去,因為后面的原理需要前面的內容做為鋪墊,主要是在處理邊角問題上。
先看下效果,要不然各位可能沒動力讀下去了,實在是有點長,可以試著 或者 zoom 一下看看動態效果: Cats
PS:文中的一些 demo 為了方便展示源代碼用了 jsbin,但 jsbin 偶爾抽風會顯示不出效果,試著在源代碼編輯框里不改變代碼意思的情況下編輯一下(比如在最后打一下回車)應該就可以了,或者查看一下你瀏覽器的翻墻設置,因為里面引入了 CDN 上的文件,有可能是因為 js 加載不成功導致的。
PS2:Demo 中用到的所有圖片都來自 網站,圖片版權歸原作者所有。圖片地址末尾的數字即為其在 上的 id,如果你喜歡某張圖片,可以通過 [id]/ 這個地址訪問到圖片原始頁面。
好了,正文開始。
開始之前,先對比一下三種比較常見的圖片布局的差異:
花瓣:
,500px,圖蟲等,以 為代表的即不等寬也不等高的圖片布局有如下特點:
以上介紹的前兩種布局都有一個共同點,那就是圖片沒有經過非等比拉伸,也就是說圖片里的內容沒有變形,也沒有被裁剪,只是放大或者縮小,這是目前圖片類應用在展示圖片上的一個趨勢,應該說,很少有專做圖片的網站會把照片非等比拉伸顯示(變形拉伸真的給人一種殺馬特的感覺…),最次的展示方式也就是把圖片裁剪成正方形后展示在一個正方形的區域里,類似于正方形容器的 -size: cover; 的效果。
另外,在花瓣的布局中,比較寬的圖片展示區域會比較小;而在第二種布局中,則是比較高的圖片展示區域會比較小。
但是,在第一種布局中,因為寬度是定死了的,所以高寬比小到一定程度的圖片,顯示區域會非常小。而在第二種布局中,因為不同行的高度是不一樣的,如果比較高的圖片出現在比較高的行,還是有可能展示的稍大些的。
總體來說,以 為代表的圖片布局,在顯示效果上更優。關于如何使用 JS 來完成 / 500px 布局的算法,這里就不討論了,讀者可以自己思考一下~
思考完成后可以看看這個頁面對這個布局的動態演示,打開頁面,等圖片全部加載完成后 點擊頁面頂部的 按鈕。
Demo 演示了如下的布局方式:先按照相同的高度把圖片排列起來,然后按行對每行的圖片進行等比放大,放大到當前行的所有圖片正好跟容器兩邊對齊,布局完成。
OK,下面根據上面的分析稍微總結一下評判圖片布局優劣的一些標準:
第一次看到類似 照片列表的布局已經不記得是在哪里了,當時只是覺得這種布局肯定需要 JS 參與,因為每行圖片高度相同的情況下不可能那么恰到好處的在容器兩端對齊,且所有圖片之間的間距大小也一樣(如果間距大小不一樣但兩端對齊,可以使用 的圖片加上 text- 來實現,在圖片較小的時候(比如搜索引擎的圖片結果)也不失為一種選擇)。然而通過觀察,發現每行的高度并不相同,就確認了必然需要 JS 參與才能完成那樣的布局。
然而當越來越多的開始網站使用這樣的布局時,做為一個熱衷于能用 CSS 實現就不用 JS 的前端工程師,我就在考慮,能否僅用 CSS 實現這樣的布局呢?尤其是不要在 時重新計算布局。
在經過一些嘗試后,我發現可在一定程度上用純 CSS 實現類似的布局。這里說的一定程度上僅使用 CSS 實現布局,我的意思是:布局一但渲染完成,布局后序的 ,zoom 都可以在沒有 JS 參與的情況下保持穩定,也就是說,首次的渲染甚至可以通過服務器完成,整個過程可以沒有 JS 參與,所以說成是用純 CSS 實現也不過分。
實現過程
下面就來介紹一下我是如何只通過 CSS 一步一步實現的這個布局的:
一開始,我們將圖片設置為相同的高度:
?img {
? ?height: 200px;
?}
?
?
?
?
?
這樣并不能讓圖片在水平方向上占滿窗口,于是我想到了 flex-grow 這個屬性,讓 img 元素在水平方向變大占滿容器,整個布局也變成了 flex 的了:
div { ?display: flex;
?flex-wrap: wrap;
}
img {
?height: 200px;
?flex-grow: 1;
}
把 flex 的 flex-wrap 設置為 wrap,這樣一行放不下時會自動折行,每行的圖片因為 grow 的關系會在水平方向上占滿屏幕,效果看上去已經很接近我們想要的了,但每張圖片都會有不同程度的非等比拉伸,圖片的內容會變形,這個好辦,可以用 -fit: cover; 來解決,但這么一來圖片又會被裁剪一部分。
最終 demo:
注意圖片都被裁剪了,尤其第一張。
不過上述的 DOM 結構顯然是沒辦法在實際中使用的:
為 img 標簽增加父元素
接下來我們把 DOM 結構改成下面這樣的:
?
? ? ?
?
?
? ? ?
?
?
? ? ?
?
?
? ? ?
?
我們為圖片增加了一個容器。依然把圖片設置為定高,如此一來,每個 div 將被圖片撐大,這時如果我們給 div 設置一個 flex-grow: 1; ,每個 div 將平分每行剩余的空間,div 會變寬,于是圖片寬度并沒有占滿 div。
如果我們將 img 的 width 設置為 100% 的話html圖片大小比例設置,在 IE 和 下,div 已經 grow 的空間將不會重新分配(我覺得這是個很有意思的現象,圖片先把 div 撐大,div grow 之后又把圖片拉大),但在 下,為 img 設置了 width: 100%; 之后,grow 的空間將被重新分配(我并沒有深究具體是如何重新分配的)html圖片大小比例設置,重新分配后的結果是每個容器的寬度更加接近,這并不是我們想要的。
試了幾種樣式組合后,我發現把 img 標簽的 min-width 和 max-width 都設置為 100% 的話,在 下的顯示效果就跟 IE 和 一樣了。最后我們將 img 的 -fit 屬性設置為 cover,圖片就被等比拉伸并占滿容器了,不過與前一種布局一樣,每行的高度是一樣的,另外圖片只顯示了一部分,上下兩邊都被裁剪掉了一些。
上面布局完整的 demo。
看起來跟前一個布局沒什么兩樣,但是現在我們可以在容器內部加上一些額外的標簽來顯示圖片信息了。
在這種布局下,如果圖片高度設置的比較小,布局已經沒有什么大礙,因為圖片越小就意味著每行的圖片越多而且剩余的空間越小并且剩余空間被更多的圖片瓜分,那每個容器的寬高比就越接近圖片的真實寬高比,多數圖片都能顯示出其主要部分。
棘手的最后一行
唯一的問題是最后一行,當最后一行圖片太少的時候,比如只有一張,因為 grow 的關系,它將占滿一整行,而高度又只有我們設置的 200px,這時圖片被展示出來的部分可能是非常少的,更不用說如果圖片本身上比較高,而展示區域又比較寬的情況了。
針對這種情況,我們可以讓列表最后的幾張圖片不 grow,這樣就不至于出現太大的變形,我們可以算出每行的平均圖片數量,然后用下面的 CSS 阻止“最后一行”的圖片 grow:
div:nth-last-child(5),div:nth-last-child(4),
div:nth-last-child(3),
div:nth-last-child(2),
div:nth-last-child(1) {
?flex-grow: 0;
}
然后配合 media query,在屏幕不同寬度時,讓“最后一行”的元素個數在窗口寬度變化時也動態變化:
@media (max-width: 1000px) and (min-width: 900px) { ?div:nth-last-child(5),
?div:nth-last-child(4),
?div:nth-last-child(3),
?div:nth-last-child(2),
?div:nth-last-child(1) {
? ?flex-grow: 0;
?}
}
@media (max-width: 1100px) and (min-width: 1000px) {
?div:nth-last-child(7),
?div:nth-last-child(6),
?div:nth-last-child(5),
?div:nth-last-child(4),
?div:nth-last-child(3),
?div:nth-last-child(2),
?div:nth-last-child(1){
? ?flex-grow: 0;
?}
}
上面的代碼寫起來是相當麻煩的,因為每個屏幕寬度范圍內又要寫多個 nth-last-child 選擇器,雖然我們可以用預處理器來循環迭代出這些代碼,但最終生成出來的代碼還是有不少重復。
有沒有辦法只指定最后多少個元素就行了,而不是寫若干個 nth-last-child 選擇器呢?其實辦法也是有的,想必大家應該還記得 CSS 的 ~ 操作符吧,a ~ b 將選擇在 a 后面且跟 a 同輩的所有匹配 b 的元素,于是我們可以這么寫:
div:nth-last-child(8),
div:nth-last-child(8) ~ div {
?flex-grow: 0;
}
先選中倒數第 8 個元素,然后選中倒數第 8 個元素后面的所有同輩結點,這樣,就選中了最后的 8 個元素,進一步,我們可以直接將選擇器改寫為 div:nth-last-child(9) ~ div,就可以只用一個選擇器選擇最后的 8 個元素了。
上面的幾種選擇尾部若干元素的不同選擇器,實際上效果是不太一樣的:
選擇最后若干張圖片這種方式還是不夠完美,因為你無法確定你選擇的 flex item 一定在最后一行,萬一最后一行只有一張圖片呢,這時倒數第二行的前幾張圖片就會 grow 的很厲害(因為倒數第二行的后面 n-1 張都不 grow),或者最后兩行圖片的數量都沒有這么多,那倒數第二行就沒有元素 grow 了,就占不滿這一行了,布局就會錯亂。
那么有沒有辦法只讓最后一行的元素不 grow 呢?一開始我也了很多方法,甚至在想有沒有一個 :last-line 偽類什么的(因為有個 :first-line),始終沒有找到能讓最后一行不 grow 的方法,然而最后竟然在搜索一個其它話題時找到了辦法:
那就是在最后一個元素的后面再加一個元素,讓其 flex-grow 為一個非常大的值比如說 ,這樣最后一行的剩余空間就基本全被這一個元素的 grow 占掉了,其它元素相當于沒有 grow,更進一步,我們可以用偽元素來做這件事(不過 IE 瀏覽器的偽元素是不支持 屬性的,所以還是得用一個真實的元素做 ):
section::after { ?content: '';
?flex-grow: 999999999;
}
到這里,我們基本解決這個布局遇到的所有問題。
Demo, 或者 zoom 然后觀察最后一行的圖片。
現在這種布局下最后一行的圖片其實總是顯示完全且沒有拉伸和變形的。
但還有最后一個問題,同前一種布局一樣,如果你在線上去加載使用這種方式布局的網頁,你會發現頁面閃動非常厲害,因為圖片在下載之前是不知道寬高的,我們并不能指望圖片加載完成后讓它把容器撐大,用戶會被閃瞎眼。其實真正被閃瞎的可能是我們自己,畢竟開發時要刷新一萬零八百遍。
所以,我們必須預先渲染出圖片的展示區域(實際上幾乎所有圖片類網站都是這么做的),所以這里還是要小用一些 js,這些工作也可以在服務器端做,或者是用任何一個模板引擎(下面的代碼使用了 的模板語法)。
這個布局一旦吐出來,后續對頁面所有的動作(,zoom)都不會使布局錯亂,同時也不需要 JS 參與,符合前文所說的用純 CSS 實現:
?section {
? ?padding: 2px;
? ?display: flex;
? ?flex-wrap: wrap;
? ?&::after {//處理最后一行
? ? ?content: '';
? ? ?flex-grow: 999999999;
? ?}
?}
?div {
? ?margin: 2px;
? ?position: relative;
? ?height: 200px;
? ?flex-grow: 1;
? ?background-color: violet;
? ?img {
? ? ?max-width: 100%;
? ? ?min-width: 100%;
? ? ?height: 200px;
? ? ?object-fit: cover;
? ? ?vertical-align: bottom;
? ?}
?}
? ?// 下一行的**表達式**是計算當圖片以 200 的高度等比拉伸展示時寬度的值
? ?
我們給圖片的父容器設置與圖片比例相同的初始大小,然后為它設置 flex-grow: 1; 等待它 grow,最終的效果將與上面種布局是一樣的,但可以看到圖片在加載過程中布局是沒有抖動的:
到這里,我們總算實現了圖片的非等寬布局。Demo,注意 HTML 模板里計算寬度的表達式。
那么這個布局的展示效果究竟如何呢?
實際上我專門寫了代碼計算每張圖片被展示出來的比例到底有多少:在圖片高度為 150px 左右時,約有三分之一的圖片展示比例在 99% 以上。最差的圖片展示比例一般在 70% 左右浮動,平均每張圖片展示比例在 90% 以上。圖片越矮/屏幕越大,展示效果會越好;圖片越高/屏幕越小,展示效果就越差。
因為這種方案最后也被我拋棄了,所以就不放計算展示比例的 demo 了。
看到這里,你應該是覺得被坑了,因為這并沒有實現標題中說的 / 500px 照片列表的布局
因為每行的高度是一樣的,就必然導致大部分圖片沒有完全展示,跟 / 500px 那些高大上的布局根本就不一樣!
其實正文從現在才正式開始,下面介紹的方式也是我在實現了上面的布局后很久才想出來的,前面的內容只是介紹一些解決邊角問題用的。
可以看到,前面的實現方式并沒有讓每張圖片的內容全部都顯示出來,因為每行的高度是一樣的,而想要實現 500px 的布局,每行圖片的高度很多時候是不一樣的。
一開始我覺得,CSS 也就只能實現到這種程度了吧,直到我遇到了另一個需求:
我想用一個正方形的容器展示內容,并且希望無論瀏覽器窗口多寬,這些正方形的容器大小在一個范圍內并且總是能鋪滿窗口的水平寬度而不留多余的空間(除了元素之間的空白),乍一看這個需求可能需要 JS 參與:讀出當前瀏覽器窗口的寬度,然后計算正方形容器的 size,然后渲染。
可以看這個 demo,試著拉動一下窗口寬度然后看效果。
拉動過程中可以看到,正方形的容器會實時變大,大到一定程度后又變小讓每行多出一個正方形容器。 如果只看這一個 demo,可能各位不一定能一下子想到如何實現的,但如果只有一個正方形容器,它的邊長總是瀏覽器寬度的一半,想必很多人都知道的,長寬比固定的容器要怎么實現吧?
我們知道(事實上很多人都不確定,所以這可以做為一個面試題), 和 的值如果取為百分比的話,這個百分比是相對于父元素的寬度的,也就是說,如果我給一個 block 元素設置 -(當然,也完全可以是 -top,甚至可以兩個一起用)為 100% 的話,元素本身高度指定為 0,那么這個元素將始終是一個正方形(因為它的高度總是跟父元素的寬度一樣,而寬度 100% 也跟父元素的寬度一樣),并且會隨著容器寬度的變化而變化,想要改變正方形的大小,只需要改變父容器的寬度就可以了:
看這個 demo,拉動窗口可以看到色塊會變大,但始終保持正方形。當然,如果參照物是瀏覽器窗口,那么在現代瀏覽器中,這個效果可以用 vw / vh 實現;但如果參照物不是瀏覽器窗口,就只能用垂直 來實現了。
于是我就想到,如果不給 flex item 的元素設置高度,而是讓其被一個子元素撐開,并且這個子元素的寬度是100%,- 也是 100%,那么 flex item 以及這個用來撐大父元素的子元素就會同時保持為正方形了,于是就實現了上面的那種正方形陣列布局。
但僅僅這樣還不夠,最后一行又會出問題,如果最后一行的元素個數跟前面的行不一樣的話,它們雖然會保持正方形,但是因為 grow 的關系,會比較大,那如何保證最后一行的元素也跟前面的行大小相同呢,這時使用一個元素并設置很大的 flex-grow 讓其占滿最后一行剩余空間的做法已經不可行了,因為我們需要讓最后一行的元素恰到好處的跟前面行的元素 grow 時多出一樣的空間。
其實解決方案也很簡單,把最后一行不當最后一行就行了!此話怎講呢?
在最后添加多個占位符,保證可見的最后一個元素永遠處于視覺上的最后一行,而讓占位符占據真正的最后一行,然后把這些占位符的高度設置為 0 。具體添加多少個占位符呢?顯然是一行最多能顯示多少個元素,就添加多少個了,比如前面的 demo 就添加了 8 個占位符,你可以在源代碼里面看一下。另外為了更好的語義,其實可以用其它的標簽當做占位符,這樣就不用寫出上面那種晦澀的選擇器了。
這樣一來,始終能占滿水平寬度的正方形陣列布局也實現了。
本來我以為,到這里就結束了,即使用上最先進的 布局,CSS 也無法實現圖片不裁減不拉伸且對齊的完美布局。
- FAKE EOF -
4 月 2 號的早上我醒來的時候,突然想到,既然可以讓一個容器始終保持正方形,那豈不是也可以讓這個容器始終保持任何比例?顯然是可以的,只要我們把用于撐大父元素的那個元素的 - 設置為一個我們想要的值就可以了!這樣一來,說不定可以實現圖片布局中,所有圖片都完全展示且占滿水平寬度的布局(也就是 / 500px 的布局)!
當然,前面提到過,由于圖片加載緩慢,圖片布局方案往往都會提前知道圖片的寬高來進行容器的預渲染,然后圖片加載完成后直接放進去。
所以這里我們仍然需要用 JS 或者服務器來計算一下圖片的寬高比例,然后設置到 - 上面去,以保證容器的寬高比始終是其內部圖片的寬高比。
我們先讓所有圖片以 200px 的高度展示,寫出如下模板代碼:
style="display:flex;flex-wrap:wrap;"> ?
?這個公式計算了圖片高度為 200 時的寬度的值
? ?
? ?上面這個公式讓此元素及其父元素的比例與圖片原始比例相同,因為是垂直方向的 padding,所以是高度除以寬度,又因為是百分比,所以除以 100
?
在上面布局中,因為 flex-wrap 的關系,每一行不夠放的時候后面的內容就會折行,并且留出一些空白,每個容器的寬高比都是跟未來放入其內部的圖片的寬高比是一樣的,為了便于展示,我將圖片大小設置為容器大小的四分之一,應該明顯可以看出圖片的右下角處于容器的中心位置。
Demo:
下一步,我們只需要讓所有的容器元素都 grow 就可以了,那么是把所有的元素的 flex-grow 設置為 1 嗎?
實際上如果設置了并看了效果,我們會發現并不是,因為我們希望每行元素在 grow 的時候,保持原有比例且高度相同。
Demo:
可以看到如果給所有的 flex item 設置 flex-grow: 1; 的話,容器跟圖片的比例并不一致(雖然比較接近),這里我將圖片寬度設置了為容器的寬度以便觀察。
通過一些簡單的計算我們會發現,在每行的圖片中,每張圖片在水平方向上占用的寬度正好是其寬度在這一行所有圖片寬度之和中所占的比例。
在前面不 grow 的情況下,每張圖片的容器的寬度已經是按比例分配了,而想要實現前一行描述的分配方式,每行的剩余空間,我們希望它仍然按照目前容器寬度所占的比例來分配,于是,每個容器的 grow 的值,正好就是它的寬度,只不過不要 px 這個單位。
最終的代碼如下:
flex,wrap ?//實際上因為 flex-grow 是按比例分配,所以第二個公式里的 *200 可以不要,這要我們就只需要改前一個 200 了
?
? ?
?
這樣一來,容器會占滿當前行,并且保持與未來內部所放入的圖片相同的寬高比。
Demo,可以看到,每張圖片都被完整展示出來了:
至于最后一行怎么處理,前面已經介紹過了,用一個 flex-grow 極大的元素占滿剩余空間就可以了。
這種布局在渲染完成后,你可以放心的 和 zoom,布局都不會錯亂,而且沒有 JS 的參與。
到這里,我們終于實現了類似 / 500px 網站的圖片布局。
總結一下這個方案的原理
這種布局的優點:
最后說一下這種方案的一些缺點:
關于降級
由于 IE 9 都是不支持 的,所以這個方案必然需要優雅降級。在不支持的瀏覽器上,讓圖片都以正方形展示應該也不會太差,然后用 float 或者 -block 來折行,這里就不細說了。
最后,本文其實只實現了 500px 的圖片的布局(即所有圖片在一個容器里),實際上并沒有實現 的布局, 的布局比 500px 的還要復雜很多,仔細觀察就會發現,其是按日期排列并且不同日期在同一行顯示的時候也可以兩邊對齊,這種布局后來我也有了純 CSS 的解決方案。如果各位意猶未盡,可以在文后留言,我會將實現方案再整理一篇文章出來~
本文到此結束,謝謝圍觀!文中如有紕漏之處,還請各位大神留言指正~
最后的最后,廣告時間:
本人決定創業開辦前端培訓班,地點杭州,9月20左右開課,費用優惠包住宿,詳情請點擊我的專欄文章:下定決心,就是要開前端培訓班,如果有朋友想學,歡迎介紹。如果沒有也希望你能進去點個贊讓更多人看到~寫文章不易,創業更不易~先行謝過了!