CSS 子元素依次收縮的實現
當子元素的寬度(或高度)超過父元素時,如果父元素設定了 display: flex
,則子元素將按比例縮小自己的寬度(或高度),但現在我們希望子元素按一定的優先順序縮小。即:當寬度不足時,優先縮小某一個子元素,當達到該元素的最小寬度(min-width
)時,再開始縮小另外的元素。下面是一個示例圖:
可以看到,當寬度不足時,上例中優先縮小了最右的元素,當最右元素達到最小寬度 100px 時開始縮小左邊的元素,依此類推。
本文我們將分析 flexbox 中的 flex-shrink
屬性,來實現上述效果。本文假設你熟悉 flexbox 的基本用法。
#基本設定
話不多說,先把主要的框架搭上,參見下面的
在上面的例子中,只設置了父元素為 display: flex
及第一個元素為 flex: none
,即不參與 flexbox 的伸縮的計算,只佔與它本身寬度相同的寬度。
如果在輸入框中輸入,可以看到:當寬度不足時,三個子元素一起收縮,出現 ...
字樣。這是因為,flex 元素的子元素的預設 flex 設定為 flex: 0 1 auto
。即收縮的比值為 1
。
題外話,設定 flex
屬性相當於設定 3 個值,即 flex: flex-grow, flex-shrink, flex-basis
。所以官方提倡直接設定 flex
屬性而不是分別設定三個。
#內建的支援?
我們通過查詢文件發現,flex-shrink
flex-shrink: 2
而元素 B 為 flex-shrink: 1
,當需要縮小 30px 才能不超出父元素寬度時,A 與 B 按 2:1
收縮,即 A 元素縮小 20px 而 B 元素縮小 10px。
這樣看來,flex-shrink
並不適合我們上面所說的任務。因為只要設定了
flex-shrink,那麼元素就一定會縮小,也就達不到前文需要的效果了。
也許你從標題裡也猜到了,是的,flexbox 並沒有提供任何機制,來設定伸縮的優先順序。所以我們只能進一步思考,如何才能做到?
#合適的比例?
於是我們的第一反應就是:如果把其中一個元素的 flex-shrink
flex:none
,第三個元素設定為 flex: 0 1
,第四個元素 flex: 0 1000
,即它的縮小的係數是第三個元素的 1000 倍!
輸上幾個字元,居然就成功了!
然後如果仔細觀察輸入的情況,會發現輸入到一定長度時,雖然最後一個元素還沒有達到
min-width
,第三個元素也發生了收縮:
難道是縮小系數還不夠大?調成 10000 試試。還真的就成功了!但為什麼 1000/1 不夠大,而 10000/1 就能正常工作;需要多大的比例還能實現一個元素優先收縮的效果?
#flex-shrink 如何計算?
顯然我們之前據說的計算方法是錯誤的,那 flex-shrink
真正的作用方式如何呢?
首先,縮放的係數是由 flex-shrink
的值乘於 flex-basis
的值共同決定的,而這麼做的原因即使指定了相同的 flex-shrink
,較大的元素(較大的 basis)也會顯得收縮了更多,也符合人的直觀印象。
得到這個比例之後,再除於所有子元素的係數之和作為最終的縮小的比例,用這個比例乘於總共需要縮小的寬度,就是該元素需要縮小的寬度了。還是看看例子吧:
.flex-container{ width: 600px; }.flex-item-1{ flex-basis: 100px; flex-shrink: 1; }.flex-item-2{ flex-basis: 400px; flex-shrink: 1; }.flex-item-3{ flex-basis: 400px; flex-shrink: 1; }Total basis: 900pxSpace remaining: -300pxItem 1 shrink factor: (1×100) / (100px + 400px + 400px) = .111 × -300px = -33.333pxItem 2 shrink factor: (1×400) / (100px + 400px + 400px) = .444 × -300px = -133.333pxItem 3 shrink factor: (1×400) / (100px + 400px + 400px) = .444 × -300px = -133.333px |
#寬度可以是小數?
我們知道 CSS 的最小單位是畫素,即 px
,那計算出來的小數有何作用?經過一番搜尋,又學習到了新的知識:
getBoundingClientRect(),它可以返回一個 DOM 元素的位置和大小資訊,最為重要的是,它返回資訊的型別是 浮點型!這意味著瀏覽器是需要儲存小數級別的資訊的!
我們打印出了一些資訊:
下面是在我本機上得到的輸出結果,下面是在第三個元素縮小之前:
下面是第三個元素縮小之後:
上面這兩張圖只能對比第三個元素的資訊,這裡直接說下,在第四個元素收縮之前,它的大小是 191px
。在第三個元素收縮時(即變成 ...
)時:
總寬度: 600px (去掉父元素的 border 2px)總縮小寬度:600-60-176-186-191 = -13px第3個元素的 shrink factor: (1×186) / (186px + 1000×191px) = 0.00094674680015273484 × -13px = -0.01230770840198555292px第4個元素的 shrink factor: (1000×191) / (186px + 1000×191px) = 0.99905325319984726515 * -13px = -12.98769229159801444695px |
因此,計算後第 3 個元素的寬度為 186 - 0.01230770840198555292 = 185.98769229159801444708
並不等於圖片中的結果 185.98333740234375
。恭喜,發現了一個很深的坑。
問題其實在於,瀏覽器會保留多少精度。這是一個沒有標準定義的內容,叫作 “subpixel
rendering”。一個簡單的例子就是指定 3 個 width: 33.33333%
的 div 時,由於精度問題,瀏覽器可能並不會佔滿 100%。
那精度到底是多少呢?這個精度叫作
LayoutUnit,Chrome 是 1/64px
,而
Firefox 是 1/60px
。
我們這裡取整:
185.98769229159801444708 * 60=> 11159.2615374958808668248011159 / 60=> 185.98333333333333333333 |
當然,由於 1/60
的精度是無限的,還是會有精度丟失,這裡看到 185.98 < 186
因此導致元素 3 發生了 overflow。
最後,如果你願意計算,這個 flex-shrink 的大小是跟各個元素的寬度相關的,在這個特定的例子裡,假設元素 3 寬度為 a
,元素 4 寬度為 b
,元素 4 的 min-width
為 c
,則要使元素 3 保持正常,則要滿足 a/(a + bx) * (b-c) < 1/60
,即使 a
元素縮小的量小於一個 LayoutUnit,即 x > (60*a*(b-c) -a)/b
,算得約 5316。
當然,由於我們輸入的字元並不是 1px 的,所以可能相差幾倍也不太重要。
#小結
- 想讓子元素按優先順序收縮,可以通過設定大倍率的 flex-shrink 完成。
- flex-shrink 的演算法與 flex-grow 不同,需要先與 flex-basis 相乘得到 shrink factor。
- 瀏覽器的 pixel 最小單位稱為 LayoutUnit,Chrome 為
1/64px
,Firefox 為1/60px
。