css flex布局 精确计算成员宽度值

4,414 阅读15分钟

前言

css 的 flex 弹性布局,广泛应用在当今的页面开发中,其弹性伸缩的的灵活性非常擅长于开发有响应式需求的网站。但是有很多时候,其灵活性也会让人疑惑,特别是确定尺寸时,基本上是靠不断尝试。为了在开发时更加得心应手,下面将以主轴方向上,也就是默认布局方向上的成员宽度值计算为例子进行研究。

准备

flex 是弹性的伸缩布局,具有伸和缩两个特性,下面在研究时也以伸和缩两种状态来进行研究。 在开始之前,先说一下一些定义,类名 container 是 flex 容器, 类名 item 是 flex 的成员对象,每个成员对象内嵌了自已的内容。每个成员都设置了相应的外边距、边框、内边距,为了方便表述把每个成员的外边距宽度的左右宽度和表述为 margin,类似的表述为 border、padding,在下面的计算中会有提及。最后把每个成员的内容区域宽度表述为 content。在下面的例子中我们会发现,成员的最终宽度大小是由成员的 width flex-basis 和 content 值共同决定,在不同状态下也会分别受到 flex-grow 和 flex-shrink 的影响 

下面的 flex 布局中设置了 6 个成员,每个成员都设置了不同的 width flex-basis content flex-grow 值

<div class="wrapper">
  <h1>test</h1>
  <style>
    .container { display: flex; background-color: #915151;}
    .item { height: 80px; text-align: center; background-color: rgb(214, 120, 52);border-radius: 1em; border: 5px solid #000;text-align: left;margin: 15px; padding: 10px }  
</style>
  <div class="container">
    <div class="item" style="flex-grow: 1;width: 100px; flex-basis: 0">     
      <div style="width: 40px"></div>
    </div>
    <div class="item" style="flex-grow: 0; width: 250px; flex-basis: 200px">      
        <div style="width: 300px"></div>    
    </div>
    <div class="item" style="flex-grow: 2; flex-basis: 200px">      
        <div></div>    
    </div>
    <div class="item" style="flex-grow: 2;  width: 70px">      
        <div></div>    
    </div>
    <div class="item" style="flex-grow: 2;">      
        <div style="font-family: monospace;">12345678910111213141516</div>    
    </div>
    <div class="item" style="flex-grow: 3; width: 200px;">      
        <div style="width: 100px"></div>    
    </div>
  </div>
</div>

下面是生成布局的截图,包括了每个成员的尺寸







最内层的区域就是成员的大小区域,也就是成员的宽度和高度值,从宽度值可以看出,有的元素的宽度值和成员的 content 大小一样, 有的和成员的 width 值一样,有的和两者都不同,下面将通过公式来计算各个成员的最终宽度值。

由于不是每个成员都设置了相应的 flex-basis、width 和 内容的 width 值,所以我先定下下面的规则来补充缺失的参数值,设置 t-flex-basis 为转化的 flex-basis 值, t-width 为转化的 width 值,t-content 为转化的 content 值

  1. 成员的 flex-basis 值存在, 那么 t-flex-basis 值就是该值,如果不存在,那么 t-flex-basis 值就是 t-width 的值
  2. 成员的 width 值存在, 那么 t-width 的值就是该值,如果不存在,,那么 t-width 值就是成员内容设置的宽度值,如果成员内容没有设置宽度值,那么就取成员内容的 max-content 尺寸,所谓的 max-content 尺寸可以理解为设置 white-space: nowrap 下的尺寸 
  3. 成员的 content 设置了 width 值, 那么 t-content 的值就是该值,如果不存在, 那么 t-content 值就是内部的首选最小尺寸值,像成员 3, 4 内部没有字符,那么其 t-content 就是 0,而成员 5 有一串不断行的字符,我故意把字体设置为等宽字体,在我的浏览器中每个字符是 6.5px,这里有 23 个字符,所以 t-content 就为 149.5px

通过以上规则,就可以把成员的参数值转化为下表的数据


下面可以开始计算最终宽度值

计算步骤

  1. 获取剩余空间
  2. 分配剩余空间
  3. 找出不符合分配的成员,重新计算分配空间


获取剩余空间

剩余空间 = 容器可分配空间 -  每个成员(margin + border + padding + t-flex-basis)的和

在我这里的容器宽度是 1349px,这个也是这里的容器可分配空间

剩余空间 = 1349 - (30 + 10 + 20 + 0) - (30 + 10 + 20 + 200) - (30 + 10 + 20 + 200) - (30 + 10 + 20+ 70) - (30 + 10 + 20 + 149.5) - (30 + 10 + 20 + 200)  = 169.5

分配剩余空间

分配的剩余空间 = 剩余空间 * (当前成员的 flex-grow) / 待分配成员的 flex-grow 值之和

item1 分配的剩余空间  = 169.5 * 1 / (1 + 0 + 2 + 2 + 2 + 3) = 16.95

item2 分配的剩余空间 = 169.5 * 0 / (1 + 0 + 2 + 2 + 2 + 3) = 0

item3 分配的剩余空间 = 169.5 * 2 / (1 + 0 + 2 + 2 + 2 + 3) = 33.9

item4 分配的剩余空间 = 169.5 * 2 / (1 + 0 + 2 + 2 + 2 + 3) = 33.9

item5 分配的剩余空间 = 169.5 * 2 / (1 + 0 + 2 + 2 + 2 + 3) = 33.9

item6 分配的剩余空间 = 169.5 * 3 / (1 + 0 + 2 + 2 + 2 + 3) = 50.85

找出不符合分配的成员,重新计算分配空间

所谓不符合分配的成员指的是 (分配的剩余空间 + t-flex-basis) < min(t-content, t-width)

当成员不符合分配时,那么该成员的最终值就是 t-content 和 t-width 中的最小值

否则成员的最终值就是:分配的剩余空间 + t-flex-basis 

通过比较可以得到 item1 不符合分配

item1 (0 + 16.95) < min(t-content,t-width) = 40

所以 item1 的最终宽度是 40

由于 item1 如果是按分配应该是 16.95,现在是 40,多占据了 23.05

所以要重新分配空间,分配的空间中要减少相应比例的空间

item2 重新分配后的剩余空间 = 0 - 23.05 * 0 / (0 + 2 + 2 + 2 + 3) = 0

item3 重新分配后的剩余空间 = 33.9 - 23.05 * 2 / (0 + 2 + 2 + 2 + 3) = 28.78

item4 重新分配后的剩余空间 = 33.9 - 23.05 * 2 / (0 + 2 + 2 + 2 + 3) = 28.78

item5 重新分配后的剩余空间 = 33.9 - 23.05 * 2 / (0 + 2 + 2 + 2 + 3) = 28.78

item6 重新分配后的剩余空间 = 50.85 - 23.05 * 3 / (0 + 2 + 2 + 2 + 3) = 43.17

通过比较可以得到 item2 不符合分配

item2 (0 + 200) < min(250, 300) = 250

所以 item2 的最终宽度是 250

由于 item2 如果是按分配应该是 200,现在是 250,多占据了 50

所以要重新分配空间,分配的空间中要减少相应比例的空间

item3 重新分配后的剩余空间 = 28.78 - 50 * 2 / (2 + 2 + 2 + 3) = 17.67

item4 重新分配后的剩余空间 = 28.78- 50 * 2 / (2 + 2 + 2 + 3) = 17.67

item5 重新分配后的剩余空间 = 28.78- 50 * 2 / (2 + 2 + 2 + 3) = 17.67

item6 重新分配后的剩余空间 = 43.17 - 50 * 3 / (2 + 2 + 2 + 3) = 26.5

通过比较可以得到以上的成员都符号分配

item3 的最终宽度是 17.67 + 200 = 217.67

item4 的最终宽度是 17.67 + 70= 87.67

item5 的最终宽度是 17.67 + 149.5= 167.17

item6 的最终宽度是 26.5 + 200 = 226.5

可以看到计算结果和最终的结果是一致的

以上是关于在伸长状态下成员的计算规则,那么在收缩状态下,上面的计算公式还能起作用吗


下面的 flex 布局中设置了 6 个成员,每个成员都设置了不同的 width flex-basis content flex-shrink 值, 为了让元素处于收缩状态,我修改了成员的大小

<div class="wrapper">  
    <h1>test</h1>  
    <style>    
        .container {display: flex; background-color: #915151;}
        .item {height: 80px; text-align: center; background-color: rgb(214, 120, 52); border-radius: 1em;border: 5px solid #000;text-align: left;margin: 15px;padding: 10px}  
    </style>  
    <div class="container">    
        <div class="item" style="flex-shrink: 1;width: 100px; flex-basis: 500px">      
            <div style="width: 40px"></div>    
        </div>   
        <div class="item" style="flex-shrink: 0; width: 250px; flex-basis: 300px">      
            <div style="width: 300px"></div>    
        </div>    
        <div class="item" style="flex-shrink: 2; flex-basis: 500px">      
            <div></div>    
        </div>    
        <div class="item" style="flex-shrink: 2;  width: 400px">      
            <div></div>    
        </div>    
        <div class="item" style="flex-shrink: 2;">      
            <div style="font-family: monospace;">12345678910111213141516</div>    
        </div>    
        <div class="item" style="flex-shrink: 3; width: 300px;">      
            <div style="width: 100px"></div>    
        </div>  
      </div>  
</div>

下面是生成布局的截图,包括了每个成员的尺寸






下面将通过公式计算成员最终的宽度值

按照前面提及的转化规则,得到类似下面的表格


依据上面提到的计算步骤进行计算


获取剩余空间

剩余空间 = 容器可分配空间 - 每个成员(margin + border + padding + t-flex-basis)的和

在我这里的容器宽度是 1349px,这个也是这里的容器可分配空间

剩余空间 = 1349 - (30 + 10 + 20 + 500) - (30 + 10 + 20 + 300) - (30 + 10 + 20 + 500) - (30 + 10 + 20+ 400) - (30 + 10 + 20 + 149.5) - (30 + 10 + 20 + 300) = -1160.5

分配剩余空间

分配的剩余空间 = 剩余空间 * (当前成员的 flex-shrink) / 待分配成员的 flex-shrink值之和

item1 分配的剩余空间 = -1160.5 * 1 / (1 + 0 + 2 + 2 + 2 + 3) = -116.05

item2 分配的剩余空间 = -1160.5 * 0 / (1 + 0 + 2 + 2 + 2 + 3) = 0

item3 分配的剩余空间 = -1160.5 * 2 / (1 + 0 + 2 + 2 + 2 + 3) = -232.1

item4 分配的剩余空间 = -1160.5 * 2 / (1 + 0 + 2 + 2 + 2 + 3) = -232.1

item5 分配的剩余空间 = -1160.5 * 2 / (1 + 0 + 2 + 2 + 2 + 3) = -232.1

item6 分配的剩余空间 = -1160.5 * 3 / (1 + 0 + 2 + 2 + 2 + 3) = -348.15

找出不符合分配的成员,重新计算分配空间

所谓不符合分配的成员指的是 (分配的剩余空间 + t-flex-basis) < min(t-content, t-width)

当成员不符合分配时,那么该成员的最终值就是 t-content 和 t-width 中的最小值

否则成员的最终值就是:分配的剩余空间 + t-flex-basis

通过比较可以得到 item5 不符合分配

item5 (-232.1 + 149.5) < min(t-content,t-width) = 149.5

所以 item5 的最终宽度是 149.5

由于 item5 如果是按分配应该是 -82.6,现在是 149.5,多占据了 232.1

所以要重新分配空间,分配的空间中要减少相应比例的空间

item1 重新分配后的剩余空间 = -116.05 - 232.1 * 1 / (1 + 0 + 2 + 2 + 3) = -145.0625

item2 重新分配后的剩余空间 = 0 - 232.1 * 0 / (1 + 0 + 2 + 2 + 3) = 0

item3 重新分配后的剩余空间 = -232.1 - 232.1 * 2 / (1 + 0 + 2 + 2 + 3) = -290.125

item4 重新分配后的剩余空间 = -232.1 - 232.1 * 2 / (1 + 0 + 2 + 2 + 3) = -290.125

item6 重新分配后的剩余空间 = -348.15 - 232.1 * 3 / (1 + 0 + 2 + 2 + 3) = -435.1875

通过比较可以得到 item6 不符合分配

item6 (-435.1875 + 300) < min(300, 100) = 100

所以 item6 的最终宽度是 100

由于 item6 如果是按分配应该是 -135.1875,现在是 100,多占据了 235.1875

所以要重新分配空间,分配的空间中要减少相应比例的空间

item1 重新分配后的剩余空间 = -145.0625 - 235.1875 * 1 / (1 + 0 + 2 + 2) = -192.1

item2 重新分配后的剩余空间 = 0 - 235.1875 * 0 / (1 + 0 + 2 + 2) = 0

item3 重新分配后的剩余空间 = -290.125- 235.1875 * 2 / (1 + 0 + 2 + 2) = -384.2

item4 重新分配后的剩余空间 = -435.1875 - 235.1875 * 2 / (1 + 0 + 2 + 2) = -529.2625

通过比较可以得到 item4 不符合分配

item4 (-529.2625 + 400) < min(t-content,t-width) = 0

所以 item4 的最终宽度是 0

但是可以看到 item4 实际的渲染结果不是 0,所以以上的计算公式在收缩状态下是无效的,问题出现在计算剩余空间的计算上,在收缩状态下的计算公式是

分配的剩余空间 = 剩余空间 * (当前成员的 flex-shrink * t-flex-basis) / 待分配成员的(flex-shrink * t-flex-basis)之和


重新计算以上的步骤

分配剩余空间

分配的剩余空间 = 剩余空间 * (当前成员的 flex-shrink * t-flex-basis) / 待分配成员的(flex-shrink * t-flex-basis)之和

item1 分配的剩余空间 = -1160.5 * 1 * 500 / (1 * 500 + 0 * 300 + 2 * 500 + 2 * 400 + 2 * 149.5 + 3 * 300) = -165.83

item2 分配的剩余空间 = -1160.5 * 0 * 300 / (1 * 500 + 0 * 300 + 2 * 500 + 2 * 400 + 2 * 149.5 + 3 * 300) = 0

item3 分配的剩余空间 = -1160.5 * 2 * 500 / (1 * 500 + 0 * 300 + 2 * 500 + 2 * 400 + 2 * 149.5 + 3 * 300) = -331.666

item4 分配的剩余空间 = -1160.5 * 2 * 400 / (1 * 500 + 0 * 300 + 2 * 500 + 2 * 400 + 2 * 149.5 + 3 * 300) = -265.33

item5 分配的剩余空间 = -1160.5 * 2 * 149.5 / (1 * 500 + 0 * 300 + 2 * 500 + 2 * 400 + 2 * 149.5 + 3 * 300) = -99.168

item6 分配的剩余空间 = -1160.5 * 3 * 300 / (1 * 500 + 0 * 300 + 2 * 500 + 2 * 400 + 2 * 149.5 + 3 * 300) = -298.5

找出不符合分配的成员,重新计算分配空间

所谓不符合分配的成员指的是 (分配的剩余空间 + t-flex-basis) < min(t-content, t-width)

当成员不符合分配时,那么该成员的最终值就是 t-content 和 t-width 中的最小值

否则成员的最终值就是:分配的剩余空间 + t-flex-basis

通过比较可以得到 item5 不符合分配

item5 (-99.168 + 149.5) < min(t-content,t-width) = 149.5

所以 item5 的最终宽度是 149.5

由于 item5 如果是按分配应该是 50.33,现在是 149.5,多占据了 99.17

所以要重新分配空间,分配的空间中要减少相应比例的空间

item1 分配的剩余空间 = -165.83 - 99.17 * 1 * 500 / (1 * 500 + 0 * 300 + 2 * 500 + 2 * 400 + 3 * 300) = -181.325

item2 分配的剩余空间 = 0 - 99.17 * 0 * 300 / (1 * 500 + 0 * 300 + 2 * 500 + 2 * 400 + 3 * 300) = 0

item3 分配的剩余空间 = -331.666 - 99.17* 2 * 500 / (1 * 500 + 0 * 300 + 2 * 500 + 2 * 400  + 3 * 300) = -362.65

item4 分配的剩余空间 = -265.33 - 99.17 * 2 * 400 / (1 * 500 + 0 * 300 + 2 * 500 + 2 * 400  + 3 * 300) = -290.1225

item6 分配的剩余空间 = -298.5 - 99.17 * 3 * 300 / (1 * 500 + 0 * 300 + 2 * 500 + 2 * 400 + 3 * 300) = -326.39

通过比较可以得到 item6 不符合分配

item6 (-326.39 + 300) < min(300, 100) = 100

所以 item6 的最终宽度是 100

由于 item6 如果是按分配应该是 -26.39,现在是 100,多占据了 126.39

所以要重新分配空间,分配的空间中要减少相应比例的空间

item1 分配的剩余空间 = -181.325 - 126.39 * 1 * 500 / (1 * 500 + 0 * 300 + 2 * 500 + 2 * 400) = -208.8

item2 分配的剩余空间 = 0 - 126.39 * 0 * 300 / (1 * 500 + 0 * 300 + 2 * 500 + 2 * 400) = 0

item3 分配的剩余空间 = -362.65 - 126.39* 2 * 500 / (1 * 500 + 0 * 300 + 2 * 500 + 2 * 400) = -417.6

item4 分配的剩余空间 = -290.1225 - 126.39 * 2 * 400 / (1 * 500 + 0 * 300 + 2 * 500 + 2 * 400) = -334.08

通过比较可以得到以上成员都符号要求

item1 的最终宽度是 500 - 208.8 = 291.2

item2 的最终宽度是 0 + 300= 300

item3 的最终宽度是 -417.6 + 500= 82.4

item4 的最终宽度是 -334.08 + 400 = 65.9

可以看到计算结果和最终的结果是一致的

总结

从以上的分析和计算中,可以看到成员的宽度受多个参数所控制,主要属性是 flex-basis, 其中在伸长和收缩状态中,计算分配的剩余空间的公式是不同的,而且在计算中还会受到成员内容大小和成员自身大小的影响,成员的最终值不得小于成员内容大小和成员自身大小中的最小值,否则就取这个最小值并重新分配剩余空间。这里可以举一个例子,很多时候我们设置 flex: 1 并希望成员都是等分的,一般情况下是没有问题的。其中 flex: 1 实际上是 flex-basis: 0%, flex-grow: 1, flex-shrink: 1,如果你不能控制成员的内容大小,当这个内容尺寸大于等分尺寸时,由于没有设置 width 值, width 值默认等于内容大小,按照以上的分析,该尺寸就会改变成员宽度,造成等分的效果失效,要避免这种情况,可以设置 width 的值为 0,这样成员内容大小和成员自身大小中的最小值就为0,等分尺寸肯定大于该值,成员尺寸便取等分尺寸,当然这个时候成员内容就会溢出。


通过以上的分析,我们对计算成员宽度已经有初步的了解,基本上能应对大部分的情况,但是 flex 布局还不止以上的属性,而且不同的属性之间还能互相组合,甚至在有的时候可能在容器上设置了绝对定位(flex功能不会消失),或者成员上设置了 box-sizing(设置的 width 值可能不同于读取的 width 值),或者 width 值是百分比(父元素宽度不确定时,width 的表现像 auto,等父元素确定后才开始计算),flex-grow 或者 flex-shrink 是小数(满足特定情况计算规则会改变)等,在以上这些情况下计算时要考虑因素会更多。还有一点要注意的是,以上布局是在默认父元素是 nowrap 情况下,以上规则可能看上去有点复杂,但是在实际开发中,我们只要记住影响成员宽度的参数就可以了,实在不符合预期才去调节对应的参数。

为了研究 flex 布局规则,我特意写了一个测试页面,大家有兴趣也可以去看一下

flex 布局测试 changk99.github.io/flexbox/