背景
我们经常在某些业务场景中,需要实现子元素均等占用父元素空间的效果。到目前为止,我使用最多的就是用 flex: 1
来实现。直到有一天我突然想使用 flex-grow: 1
来达到相同的效果,但是浏览器却给了我不一样的表现,我才发现我忽略了**剩余空间(free space)**这个关键词。一直以来,我是以 flex container 的宽度为基础来考虑空间分配的问题,简单得将 flex: 1
与 flex-grow: 1
划上了等号。
本文是重新学习了 flexbox 之后做的总结,主要围绕 flex
,flex-basis
,flex-grow
以及 flex-shrink
这 4 个属性。
在 Firefox 审查 flexbox 布局的节点可以看到一些尺寸数据,但是 Chrome 好像并不能,所以文中的代码都会在 Firefox 中审查,我的 Firefox 版本是 65.0.2。
flexbox 基本概念
关于一些基础知识,其他文章已经有详细的说明,本文就只是简单表述一下。
-
首先,设置了
display: flex
的元素会成为一个 flex container,并创建一个 FFC(Flex Formatting Context),而该元素本身则会表现为一个块级元素,相应的,设置了inline-flex
的元素则表现为内联元素。 -
flex container 内部的元素会成为 flex item,包括内部的纯文本(会被包裹一个匿名块级盒子)。绝对定位元素因为不会参与到 flex 布局中来,所以不会成为一个 flex item。
-
最后,在进行 flex layout 时,
flex
会生效,使 flex item 完全填充至 flex container 的可用空间,或者收缩 flex item 以阻止溢出。当所有的 flex item 确定了尺寸之后,就会根据justify-content
、align-self
等属性进行排列和布局。
flex-basis
flex-basis
会代替 flex item 在主轴(main axis)方向上的尺寸属性(width/height),并在分配剩余空间(free space)之前初始化 flex item 的主轴尺寸。
接受的值与 width
或者 height
相同,比如数值、百分比、auto
、min-content
以及 max-content
,另外还有 content
。
-
auto
被指定为
auto
时,会取当前 flex item 主轴尺寸属性(width/height)的值,如果取到的值是auto
时,则会使用content
,也就是根据 flex item 的内容大小来确定。比如以下结构:<div style="display: flex; width: 200px; border: 1px dotted #333;"> <div id="first" style="background: #eee; width: 80px; flex-basis: auto;"> short text </div> <div id="second" style="background: #ccc; flex-basis: auto;"> very looooooooong text </div> </div>
使用 Firefox(支持
content
,但是min-content
和max-content
是无效值)审查节点的时候可以看到: 因为设置了width: 80px
,所以div#first
的基础尺寸是 80px,而div#second
的宽度是auto
,所以它的基础尺寸就基于它的内容得到,即内容尺寸。 -
content(兼容性不好)
被指定为
content
时,是根据 flex item 的内容大小来确定尺寸的。相当于使用了width: max-content
。Chrome 是不支持该值的,Firefox 可以使用该值。
flex-grow
简单讲,flex-grow
是指当前 flex item 可以从 flex container 的剩余空间(positive free space)里分配到多少比例。
这里有一个关键词是positive free space,而我之前误解成了 flex container 的整个 content box。
例如 flex-grow: 1
就是指希望分配到 100% 的 positive free space,flex-grow: 0.5
就是指 50%,以此类推。所以,当所有的 flex-grow
总和小于 1 时,这些 flex items 也就没有分配到 100% 的 positive free space,就会有空间被剩下;当所有的 flex-grow
总和大于 1 时,就会以 100% 为基础重新计算比例,毕竟不会有超过 100% 的空间可以分配。
flex-shrink
MDN 中的定义:
flex-shrink
属性指定了 flex 元素的收缩规则。flex 元素仅在默认宽度之和大于容器的时候才会发生收缩,其收缩的大小是依据flex-shrink
的值。
可以用 negative free space 来表示溢出的空间。与 flex-grow
不同的是,如果指定 flex-shrink: 1
,不能简单的理解为这个 flex item 希望分配到 100% 的 negative free space。这里的 1 相当于一个权重,具体的计算过程会在如何计算弹性中描述。
在收缩规则中,有一个概念是 content-based minimum size,直译过来是基于内容的最小尺寸。它限制了 flex item 可以收缩的程度,不会让 flex item 收缩到更小的尺寸。它的值可能是一个确定的 主轴(main axis)尺寸属性值(width/height),也可能是 min-content
。这个概念会在如何计算弹性中使用到。
flex
flex: 0 1 auto
是它的初始值。也就是说 flex item 在默认状态下,可以在主轴维度上缩小但不能拉伸,并且根据 width/height
来确定其大小。
而被我误解为等价于 flex-grow: 1
的 flex: 1
,完整的值是 flex: 1 1 0
(flex: <positive-number> 1 0
)。
如何计算弹性
<div style="display: flex; width: 200px; border: 1px dotted #333;">
<div id="first" style="background: #eee;">
short text
</div>
<div id="second" style="background: #ccc; width: 280px;">
very looooooooong text
</div>
</div>
以上述代码为例:
-
确定主轴(main axis)的可用空间(available space),也就是 flex container 的 content box,可能是一个确定的尺寸,也可能需要计算得出。
例子中的可用空间就是 200px。
-
根据
flex-basis
确定每一个 flex item 的基础尺寸(flex base size)。由于
div#first
的flex-basis
和width
都为auto
,所以他的基础尺寸就是它的内容尺寸,通过 Firefox 审查节点可以看到它的内容尺寸是 59.93px。而div#second
由于设置了width: 280px
,所以 280px 就是它的基础尺寸。另外,在这一步骤中,最大最小约束(这个例子是
min-wdith
和max-width
)会被忽略。如果给div#first
一个min-width: 100px
,它的基础尺寸依然是 59.93px。至于这些约束何时生效,可以在后面的步骤看到。 -
**计算 flex item 的主轴尺寸(main size)总和,确定使用
flex-grow
还是flex-shrink
。例子中,可以得到总和是 339.93px,超过了 flex container 的尺寸,所以可以确定
flex-shrink
生效。 -
确定 positive free space 或者 negative free space 的大小。
首先,需要计算例子中
flex-shrink
的总和,得到值为 2。因为大于 1,所以 negative free space 的值就是 339.93px - 200px = 139.93px。 -
为每个 flex item 分配 free space,确定修改后的尺寸。
因为在这个例子中生效的是
flex-shrink
属性,所以每个 flex item 的最终大小是基础尺寸减去它所分配到的 negative free space。那么,每一个 flex item 应该被分配到多少呢? 在这个公式里,可以看到基础尺寸与flex-shrink
的值做了相乘,也就是上文flex-shrink中说到的权重。可以简单套用这个公式:div#first
: (339.93 - 200) * 59.93 / (59.93 + 280) = 24.67pxdiv#second
: (339.93 - 200) * 280 / (59.93 + 280) = 115.26px
所以最后
div#first
的宽度是 35.27px,div#second
是 164.73px。 在这个例子中,以上就是最终尺寸了,可以在 Firefox 审查节点看到这个结果。但是,如果将
div#second
的宽度调整到1000px
,就会有接下来的这一步,我们可以看到flex-shrink中提到的 content-based minimum size 会起到限制作用,步骤 2 中的最大最小约束也会生效。 -
根据 min/max-width(min/max-height) 或者 content-based minimum size 修正 flex item 的尺寸。
我们把
div#second
的width
设置为 1000px,然后套用上面的公式:div#first
: (1059.93 - 200) * 59.93 / (59.93 + 1000) = 48.62pxdiv#second
: (1059.93 - 200) * 1000 / (59.93 + 1000) = 811.31px
如果没有把这一步骤写出来,也许你会认为
div#first
的最终尺寸就是 59.93 - 48.62,div#second
是 1000 - 811.31。我们可以在 Firefox 审查一下:div#first
的内容尺寸以及弹性的确和我们预想的一样,而最终尺寸却是 31.97px。在这里就是 content-based minimum size 生效了,它使用div#first
的min-content
作为了它的值。如果我们设置了一个确定的
width
,content-based minimum size 其实会取width
和min-content
之间的最小值,比如width: 10px
,那么div#first
的最终尺寸就是 10px。我就是希望
div#first
的最终尺寸是 59.93 - 48.62 = 11.31px,这该如何做呢?为div#first
加上min-width: 5px
,这样一来它的最终尺寸就成了 11.31px。
如果 flex-grow 生效,如何计算
以这段代码为例:
<div style="display: flex; width: 300px; border: 1px dotted #333;">
<div style="background: #eee; flex-grow: 2;">
short text
</div>
<div style="background: #ccc; flex-grow: 1;">
very looooooooong text
</div>
</div>
- 首先,主轴的可用空间是 300px。
- 每一个 flex item 的基础尺寸是它们的内容尺寸,分别是 59.93px 和 152.82px。
- flex item 的主轴尺寸总和是 212.75px,所以
flex-grow
生效。 - 确定 positive free space 的大小是 87.25px。
- 计算 flex item 分配到的 positive free space,分别是 87.25 * 2 / (2 + 1) = 58.17px 和 87.25 * 1 / (2 + 1) = 29.08px。确定最终尺寸分别是 118.1px 和 181.9px。
最后
本文做的总结基本上可以覆盖我目前碰到的业务场景了,未来也许会碰到更多的场景来调整我对 flexbox 的理解。关于 flex layout 的算法,W3C有非常详细且复杂的介绍,本文做了一定的调整和简化。如果想要了解更多,可以去W3C看看一些介绍。