你不知道的 flex-shrink 计算规则
对于 flex-shrink 我们都知道它在 flex 布局中控制 flex 盒子空间不足时子元素改如何收缩,平常开发中更多的是使用默认值 1 或者设置 0。
那设置其他值的时候会有什么效果呢,不少文章中描述都不是很细,在很长一段时间我甚至以为自己是了解它的。
开篇我们带着几个问题
1. “flex-shrink
属性定义了项目的缩小比例,当父元素主轴方向空间不足的时候,子元素们按照 flex-shrink 的比例来缩小。” 这句描述对吗?
2. 一个父元素下有两个子元素,两个子元素各占用父元素 50% 且分别有 50px、20px 的 padding。这个很简单的需求用 flex 布局如何实现?如果尝试以后和你的想象不同,那为什么会这样呢?
3. 当空间不足时,各项目具体会缩小多少?子元素 ``flex-shrink不同时有何影响?子元素宽度会对缩小有影响吗?父子元素的 margin、padding、border 会对结果有影响吗?box-sizing 的值会有影响吗?
如果你对以上的问题不能清楚的回答,或者尝试以后发现和自己想象的不同,那这篇文章对于你可能会有一些用。
首先我们看第一个问题
1. “
flex-shrink
属性定义了项目的缩小比例,当父元素主轴方向空间不足的时候,子元素们按照flex-shrink
的比例来缩小。” 这句描述对吗?
这句话描述其实不准确。
flex-shrink 决定了子元素缩小系数,但在具体计算的时候,其实它还受到了 flex base size 的影响。
w3c 对于的 flex-shrink 的描述有这样一段备注
Note: The flex shrink factor is multiplied by the flex base size when distributing negative space. This distributes negative space in proportion to how much the item is able to shrink, so that e.g. a small item won’t shrink to zero before a larger item has been noticeably reduced.
从中我们可以看到,真正使用的缩小系数其实是 flex shrink factor * flex base size。下面我们用一个例子来说明它
[ 复制代码](javascript:void(0); "复制代码")
<pre style="margin: 0px; padding: 0px; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;"><style> .box { display: flex; width: 400px; outline: 1px red solid;
} .item1 { flex: 0 2 300px; background-color: #32d6d6;
} .item2 { flex: 0 1 200px; background-color: #e2a83e;
} .item3 { flex: 0 2 100px; background-color: #b85ad0;
}
</style> ... <div class="box">
<div class="item1">1</div>
<div class="item2">2</div>
<div class="item3">3</div>
</div> </pre>
](javascript:void(0); "复制代码")
按照不准确的描述 flex-shrink` 决定了子元素缩小系数,那我们知道子元素需要的空间是 300+200+100 一共 600px,但父元素只有 400px 所以分别的负空间是 200px,或者说需要缩小 200px。三个元素
flex-shrink` 分别为 2 1 2,表面上看应该分别缩小 80 40 80,那三个元素应该 220 160 20。但事实是这样吗?
如果你也尝试一下,就会知道,实际上的效果是 180 160 60。
我们来看一下正确的计算方式:
flex-shrink * flex-base(姑且先这么写,之后会修正) => factor
2 * 300 => 600
1 * 200 => 200
2 * 100 => 200
所以三个元素真正的系数分别是 600/1000 200/1000 200/1000。200 的总额得出 120 40 40。可以看到和实际情况相符。
按照这个公式可以满足多数情况的使用,但其中还隐藏着其他规则。下面我们看第二个问题
2. 一个父元素下有两个子元素,两个子元素各占用父元素 50% 且分别有 50px、20px 的 padding。这个很简单的需求用 flex 布局如何实现?如果尝试以后和你的想象不同,那为什么会这样呢?
该问题其实是我发现自己对 flex-shrink` 不够了解,从而研究的原因。` 这个问题看起来很简单吧,估计多数人第一反应是这样:
](javascript:void(0); "复制代码")
<pre style="margin: 0px; padding: 0px; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;"><style> .box { display: flex; width: 400px; outline: 1px red solid;
} .item-2-1 { flex: 1 1; padding: 50px; background-color: #32d6d6; background-clip: content-box;
} .item-2-2 { flex: 1 1; padding: 20px; background-color: #b85ad0; background-clip: content-box;
}
</style> ... <div class="box">
<div class="item-2-1">1</div>
<div class="item-2-2">2</div>
</div></pre>
](javascript:void(0); "复制代码")
看起来收缩、放大系数都相等,两个元素应该父元素 400 像素,每个 200 对吧?我们看一下实际情况
image是不是和想象不同?
下面说明原因
w3c 里对于元素可用长度有这样的描述
dimension of the flex container’s content box is a definite size, use that; if that dimension of the flex container is being sized under a min or max-content constraint, the available space in that dimension is that constraint; otherwise, subtract the flex container’s margin, border, and padding from the space available to the flex container in that dimension and use that value. This might result in an infinite value.
我们可用看到其实计算主轴可以用长度的计算是要去除 margin, border, and padding 的,但这里的描述我觉得其实也不准确,他这里说的只是 flex container,似乎只是父元素里的 margin、border、padding。但在我的实际测试的时候,其实还包括更多
比如:直接子元素的 margin、border、padding 甚至是直接子元素的 min-width,稍缓我会说为什么
那么我们来计算一下
400 - 502 - 202 = 260(可用空间)
flex-base 为 0,flex-grow 都为 1;260*(1/2)= 130
130 + 20 + 20 = 170
那么我们要如何实现子元素含 padding 时也平分空间呢?
flex-base 的描述里有这样的一句
As another corollary, flex-basis determines the size of the content box, unless otherwise specified such as by box-sizing [CSS3UI].
可以看到 flex-basis 的数值设置的 width 其实是 box-sizing 的默认值 box-sizing: content-box; 那么理所当然会想到修改参数,改为 box-sizing: border-box; 然后 flex-base: 100%;
如此一来,两个元素都如 IE 盒模型一样,宽度包含 border padding,而且收缩、放大系数都一样,是不是就可以实现需求了呢?答案还是否定的,不但实现不了需求,甚至会出现一时间难以理解的数值
image
上面提到子元素的 padding 等值也会算在不可伸缩长度里冻结掉。为什么这么说呢,我们结合上图的原因来做解说
这个值是怎么来的呢,其经过了以下的步骤
1. 计算子元素 flex-base 所代表的实际值 => 400px
2. 那两个就是 800px,父元素 400px,主轴长度不够,flex-shrink 开始生效。但第二步却不是 flex-shrink * flex-base 得出真正的比例系数,我们需要先得到“真正的” flex base size,其实之前我们提及过
事实上,真正的 flex base size 并非单纯的是 flex-base。更准确的说,子元素 flex-base 设置后带来的 content width。比如这里 flex base size = box width 400 - padding 202 - border 0 = 360,以及两外一个 400-502=300。
3. 计算比例系数 3601=360,3001=300。所有其比例系数分别是 300/660、360/660。
4. 计算需要分配的负空间 4002-400 = 400px
5. 计算分别需要缩减的部分 400(300/660)≈181.8181、400*(360/660)≈218.1818
6. 实际宽度 400-181.8181≈218.18 400-21≈181.81
对于其原因,w3c 里对于如何计算弹性长度有这样的一个描述
> Size inflexible items. Freeze, setting its target main size to its hypothetical main size…
对此我是这样理解的,在计算子元素主轴长度的时候,有这么一些操作
把 flex container 的 margin、border、padding 所占的长度冻结,因为这些不可分配
把 子元素的 margin、border、padding 所占的空间冻结,因为这部分不会参与伸缩
剩下的空间才会作为正、负长度分配给子元素
所以我们得出更详细的压缩计算公式
flex_container_available_length = flex_container_content_width(or height)
flex_items_length = flex_item_box_width + flex_item_box_width + flex_item_box_width...
shrink_factor = (flex-shrink * flex_base_size) /((flex-shrink * flex_base_size) + (flex-shrink * flex_base_size) + ...)
will_allocate_length = flex_container - flex_items_length
flex_item_width = flex_item_box_width + flex_item_box_width * (will_allocate_length * shrink_factor)
注1:flex_item_box_width = margin + padding + border + content width
注2: flex_base_size 取决于该元素的 box-sizing 和 flex-base,其值为 border-box 时,flex_base_size = flex-base - padding - barder;其值为 content-box 时,flex_base_size = flex-base。
为了验证公式的正确性,我们随意设计一个 margin padding border box-sizing flex-shrink flex-base 多样繁杂的 demo(.flexBox)
[ 复制代码](javascript:void(0); "复制代码")
<pre style="margin: 0px; padding: 0px; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;"><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>flex-shrink demo</title>
<style> .box { display: flex; width: 400px; outline: 1px red solid;
} .item1 { flex: 0 2 300px; background-color: #32d6d6;
} .item2 { flex: 0 1 200px; background-color: #e2a83e;
} .item3 { flex: 0 2 100px; background-color: #b85ad0;
} .item-2-1 { flex: 1 1; padding: 50px; background-color: #32d6d6; background-clip: content-box;
} .item-2-2 { flex: 1 1; padding: 20px; background-color: #b85ad0; background-clip: content-box;
} .demo-3 { flex: 1 1; display: flex; align-items: center;
} .demo-3-1 { flex: 1 1; padding: 50px; background-color: #32d6d6; background-clip: content-box;
} .demo-3-2 { flex: 1 1; padding: 20px; background-color: #b85ad0; background-clip: content-box;
} .item-border-box { flex-basis: 100%; box-sizing: border-box;
} .flexBox { display: flex; width: 1500px; outline: 1px red solid;
} .flexItem-1, .flexItem-2, .flexItem-3 { flex-shrink: 2; flex-basis: 300px;
} .flexItem-1 { margin: 0 10px;
} .flexItem-2 { padding: 0 20px; border: 5px #ccc solid; box-sizing: content-box;
} .flexItem-3 { padding: 0 20px; border: 5px #ccc solid; box-sizing: border-box;
} .flexItem-4, .flexItem-5, .flexItem-6 { flex-shrink: 1; flex-basis: 200px;
} .flexItem-4 { padding: 0 10px;
} .flexItem-5 { border: 5px #ccc solid; margin: 0 10px; box-sizing: content-box;
} .flexItem-6 { border: 5px #ccc solid; margin: 0 10px; box-sizing: border-box;
} .flexItem-7, .flexItem-8, .flexItem-9 { flex-shrink: 2; flex-basis: 100px;
} .flexItem-7 { border: 5px #ccc solid;
} .flexItem-8 { padding: 0 30px; margin: 0 10px; box-sizing: content-box;
} .flexItem-9 { padding: 0 30px; margin: 0 10px; box-sizing: border-box;
}
</style>
</head>
<body>
<div class="box">
<div class="item1">1</div>
<div class="item2">2</div>
<div class="item3">3</div>
</div>
<br />
<div class="box">
<div class="item-2-1">1</div>
<div class="item-2-2">2</div>
</div>
<br />
<div class="box">
<div class="demo-3">
<div class="demo-3-1"> 1 </div>
</div>
<div class="demo-3">
<div class="demo-3-2"> 2 </div>
</div>
</div>
<br />
<div class="box">
<div class="item-2-1 item-border-box">1</div>
<div class="item-2-2 item-border-box">2</div>
</div>
<br />
<div class="flexBox">
<div class="flexItem-1" title="300 - (550*600/2770) ≈ 180.866">1</div>
<div class="flexItem-2" title="300 - (550*600/2770) ≈ 180.866">2</div>
<div class="flexItem-3" title="300 - (550*500/2770) ≈ 200.722 - 20*2 - 10*2 = 150.722">3</div>
<div class="flexItem-4" title="200 - (550*200/2770) ≈ 160.288">4</div>
<div class="flexItem-5" title="200 - (550*200/2770) ≈ 160.288">5</div>
<div class="flexItem-6" title="200 - (550*190/2770) ≈ 162.274 - 5*2 = 152.274">6</div>
<div class="flexItem-7" title="100 - (550*200/2770) ≈ 60.288">7</div>
<div class="flexItem-8" title="100 - (550*200/2770) ≈ 60.288">8</div>
<div class="flexItem-9" title="100 - (550*80/2770) ≈ 84.115 - 30*2 = 24.115">9</div>
</div>
<!-- flex_container_available_length = 1500
flex_items_length = (300+10*2) + (300+20*2+5*2) + (300) + (200+10*2) + (200+10*2+5*2) + (200+10*2) + (100+5*2) + (100+10*2+30*2) + (100+10*2)
= 2050
shrink_factor = 2770
300*2 600
300*2 600
(300-20*2-5*2)*2 500
200*1 200
200*1 200
(200-5*2)*1 190
100*2 200
100*2 200
(100-30*2)*2 80
will_allocate_length = 1500 - 2050 = -550
flex_item_width
300 - (550*600/2770) = 180.866
300 - (550*600/2770) = 180.866
300 - (550*500/2770) = 200.722 - 20*2 - 10*2 = 150.722
200 - (550*200/2770) = 160.288
200 - (550*200/2770) = 160.288
200 - (550*190/2770) = 162.274 - 5*2 = 152.274
100 - (550*200/2770) = 60.288
100 - (550*200/2770) = 60.288
100 - (550*80/2770) = 84.115 - 30*2 = 24.115 -->
</body>
</html></pre>
[ 复制代码](javascript:void(0); "复制代码")
上面的代码包括文章中所有的 demo 的代码,和第二个问题里说的需求的解决方法(其实巨简单)
``文章里留下的另一个坑,min-width 会对计算有什么影响呢?这个问题留给你自己尝试思考吧。如果想不通,也欢迎留言讨论。
最后,附上资料, 同时感谢stackoverflow的帮助。``