flexbox布局指南

零.术语概念

涉及术语:

  • 伸缩容器(flex container)

  • 伸缩项(flex item)

  • 主轴(main axis)

  • 交叉轴(cross axis)

  • 主尺寸(main size)、主尺寸属性(main size property)

  • 交叉尺寸(cross size)、交叉尺寸属性(cross size property)

  • 伸缩行(flex line)

伸缩容器display的计算值为flexinline-flex的元素,其流内孩子就是伸缩项(flex item)

A flex container is the box generated by an element with a computed display of flex or inline-flex. In-flow children of a flex container are called flex items and are laid out using the flex layout model.

(摘自2. Flex Layout Box Model and Terminology

伸缩容器中的伸缩项按行排列/对齐,每一行都是伸缩行,类似于文本换行

主轴交叉轴是两个方向,互相垂直,伸缩项沿着主轴排列。具体指横向(从左向右/从右向左)还是纵向(从上到下/从下到上)取决于flex-flowwriting mode。容器或伸缩项在主轴方向的尺寸就是主尺寸,在交叉轴方向的尺寸是交叉尺寸。例如,最常见的:

Flex Layout Box Model

其中,主轴是从左向右的,交叉轴从上到下,容器的主尺寸是其width值,容器的交叉尺寸是其height值(主尺寸属性交叉尺寸属性分别是widthheight属性)

P.S.其它尺寸相关的通用术语,见2. Terminology

一.容器属性与伸缩项

flex相关的CSS属性分为两类:作用于容器的(容器属性),与作用于伸缩项的(伸缩项属性)

容器属性

  • display: flex | inline-flex:分别用来定义块级与行内级伸缩容器盒,为元素创建伸缩格式化上下文(flex formatting context

  • flex-direction: row | row-reverse | column | column-reverse:默认row,定义伸缩容器的主轴方向,不带-reverse的表示与由writing-mode确定的行内轴(inline axis)方向相同,带的相反

  • flex-wrap: nowrap | wrap | wrap-reverse:默认nowrap,定义内容是否允许换行,并定义交叉轴方向(新行从底部还是顶部开始),带-reverse的与由writing-mode确定的方向相反

  • flex-flow: <flex-direction> || <flex-wrap>:简写属性,可以出现1个或2个值,顺序无所谓

  • justify-content: flex-start | flex-end | center | space-between | space-around:默认flex-start主轴起始端对齐,定义各行内容的主轴对齐方式,分别表示起始端、结束端、居中、各项之间均匀留空与各项左右均匀留空

  • align-items: flex-start | flex-end | center | baseline | stretch:默认stretch拉伸占满交叉轴方向的空间,定义各行内容的交叉轴对齐方式,分别表示起始端、结束端、居中、基线对齐与拉伸铺满

  • align-content: flex-start | flex-end | center | space-between | space-around | stretch:默认stretch各行均匀拉伸铺满交叉轴方向的空间,定义多行内容的整体相对于容器的对齐方式,值含义与justify-content类似(多一个stretch),只是针对行而言

与BFC和IFC相比,伸缩格式化上下文(FFC)有一些特殊性:

  • 伸缩容器的margin不与内容margin发生合并(collapse)

  • 伸缩项的floatclear都无效

  • 伸缩项的vertical-align无效

  • ::first-line::first-letter伪元素不适用于伸缩容器,并且伸缩容器自己不算作祖先元素的首行或首字母

两轴方向受writing-mode影响,比如日文与英文在相同的flex属性下效果不同,具体示例见Example 5

伸缩项属性

  • flex-growflex-shrinkflexflex-basis:都会影响伸缩项拉伸、收缩,后面专门介绍

  • align-self: auto | flex-start | flex-end | center | baseline | stretch:默认auto取容器的align-items,针对单伸缩项定义其交叉轴对齐方式,值含义与align-items相同

  • order: 整数:默认0,定义伸缩项在伸缩容器中的出现顺序(允许与源文档顺序不同),伸缩项按order值从低到高排列,相等的就按文档序

P.S.特殊地,绝对定位元素的order0处理,所以其它伸缩项的order仍会影响绝对定位元素的位置(规范这么说,但实际上目前(2018/08/09)主流浏览器似乎并没有这样做,当绝对定位元素的order为极小值处理了)

P.S.另外,order属性只影响视觉媒体(只是视觉上重新排序,而不是逻辑上的)。也就是说,在听觉媒体上,仍然是按文档序读出的,所以该属性可能会带来可访问性方面的问题

二.对齐方式

主轴方向的对齐方式,由容器的justify-content控制:

justify content example

交叉轴方向的对齐方式,由容器的align-items与伸缩项的“align-self`共同决定(后者优先):

align items example

各行在交叉轴方向的对齐方式,由align-content控制:

align content example

另外,多行场景下,每行内容独立布局,所以justify-contentalign-self属性都相对当前行,而不是伸缩容器

Once content is broken into lines, each line is laid out independently; flexible lengths and the justify-content and align-self properties only consider the items on a single line at a time.

三.伸缩性(flexibility)属性

伸缩性(flexibility)是说元素能够按照既定规则改变自身宽/高适应容器的主轴尺寸,比如拉伸填满剩余空间,或者收缩自身尺寸以适应空间不足的情况

altering their width/height to fill the available space in the main dimension.

A flex container distributes free space to its items (proportional to their flex grow factor) to fill the container, or shrinks them (proportional to their flex shrink factor) to prevent overflow.

(摘自7. Flexibility

伸缩性属性(components of flexibility),指的是影响伸缩项缩放的几个属性,分别是:

  • flex-basis(尺寸基准)

  • flex-grow(拉伸因子)

  • felx-shrink(收缩因子)

P.S.如果要让元素不可伸缩,让拉伸因子和收缩因子都为0即可(flex-grow: 0; flex-shrink: 0,或者简写属性flex: none等价于flex: 0 0 auto

flex-basis

用来设置伸缩项(flex item)所占空间基准,接受的值与width/height相同,另外还支持autocontent

  • 长度值:数值加单位的形式

  • 百分比:相对于伸缩容器的内主尺寸(inner main size)

  • inherit:取父元素该属性的计算值

  • auto:伸缩项的尺寸取自主尺寸属性(main size,指的是widthheight,取决于伸缩容器的主轴方向)

  • content:基于伸缩项的内容自动计算尺寸

content相当于基准为auto并且主尺寸也为auto时伸缩项的尺寸,所以flex-basis: content等价于:

flex-basis: auto;
/* 主轴是横向,主尺寸为width */
width: auto;
/* 或者,主轴是纵向,主尺寸为height */
height: auto;

P.S.content值是后来新增的,所以兼容性不如双auto好,可以考虑上面的替代方案

flex-basis对缩放的影响见下例:

<div style="display: flex; width: 400px">
    <span style="width: 10px; flex-basis: 0; flex-grow: 1; background-color: #ddd">10px</span>
    <span style="width: 20px; flex-basis: 0; flex-grow: 1; background-color: #ccc">20px</span>
    <span style="width: 10px; flex-basis: 0; flex-grow: 2; background-color: #eee">10px</span>
</div>

<div style="display: flex; width: 400px">
    <span style="width: 10px; flex-basis: auto; flex-grow: 1; background-color: #ddd">10px</span>
    <span style="width: 20px; flex-basis: auto; flex-grow: 1; background-color: #ccc">20px</span>
    <span style="width: 10px; flex-basis: auto; flex-grow: 2; background-color: #eee">10px</span>
</div>

在一般环境中(英文writing-mode),呈现效果如下:

| 100px | 100px |  200px |
| 100px | 110px  | 190px |

第一种场景具体布局过程为:

内容初始宽度 = 0 + 0 + 0 = 0
剩余可分配宽度 = 400 - 0 = 400
按flex-grow分配剩余宽度,从左向右依次为
400 * 1 / (1 + 1 + 2) = 100
400 * 1 / (1 + 1 + 2) = 100
400 * 2 / (1 + 1 + 2) = 200
修正各伸缩项初始宽度,依次为
0 + 100 = 100
0 + 100 = 100
0 + 200 = 200

类似地,第二种为:

内容初始宽度 = 10 + 20 + 10 = 40
剩余可分配宽度 = 400 - 40 = 360
按flex-grow分配剩余宽度,从左向右依次为
360 * 1 / (1 + 1 + 2) = 90
360 * 1 / (1 + 1 + 2) = 90
360 * 2 / (1 + 1 + 2) = 180
修正各伸缩项初始宽度,依次为
10 + 90 = 100
20 + 90 = 110
10 + 180 = 190

P.S.注意,默认flex-basis默认影响的是内容框尺寸,除非box-sizing指定了其它值

P.S.根据指定值无法计算的特殊情况(比如指定了百分比值,而包含块的尺寸不确定),当做content处理

flex-grow

拉伸因子,内容不足以占满伸缩行时依据flex-grow值确定各项将额外获得空间的比例:

拉伸比例 = 当前项的flex-grow / 当前行所有项的flex-grow之和

例如3列等比布局:

<div style="display: flex; width: 300px">
    <span style="flex: 1; background-color: #ddd">韭叶</span>
    <span style="flex: 1; background-color: #ccc">大宽</span>
    <span style="flex: 1; background-color: #eee">荞麦棱</span>
</div>

呈现效果是:

| 100 | 100 | 100 |

符合预期,这是因为flex: 1等价于flex: 1 1 0(见下文flex简写属性部分),相当于同时设置了flex-grow: 1; flex-basis: 0,其效果就是把300px都当做额外空间,并按1:1:1分配

flex-shrink

收缩因子,伸缩行装不下该行内容时依据flex-shrink与基础尺寸(见下文布局算法部分)确定各项将收缩空间的比例:

收缩比例 = 当前项的flex-shrink值 * 基础尺寸 / 当前行所有项的(flex-shrink值 * 基础尺寸)之和

收缩的情况相对麻烦一些,涉及概念较多,具体示例见下文布局算法部分

flex

flex: none | [ <‘flex-grow’> <‘flex-shrink’>? || <‘flex-basis’> ]

简写属性,默认flex: 0 1 auto默认各项按内容尺寸比例收缩,不拉伸)。由于不带单位的0对这三个属性而言都是合法的,所以flex简写属性中的flex-basis要么带单位,要么前面有两个伸缩因子值(数值),否则都会被当做伸缩因子:

flex: 0;    /* 等价于 flex: 0 1 0%; */
flex: 0px;  /* 等价于 flex: 1 1 0px; */
flex: 0 0;    /* 等价于 flex: 0 0 0%; */
flex: 0 0px;  /* 等价于 flex: 0 1 0px; */
flex: 1 1 0;  /* 等价于 flex: 1 1 0px; */

注意,奇怪的是,flex-growflex-shrink的初始值似乎会变来变去的,还与默认值不相同,这是出于方便常见场景考虑:

Note: The initial values of flex-grow and flex-basis are different from their defaults when omitted in the flex shorthand. This is so that the flex shorthand can better accommodate the most common cases.

简言之,使用flex简写属性的话,省略的子属性值会根据常见场景来赋予初始值,而不直接取默认值,例如:

  • flex: initial等价于flex: 0 1 auto

  • flex: auto等价于flex: 1 1 auto

  • flex: none等价于flex: 0 0 auto

  • flex: 正数等价于flex: 正数 1 0

P.S.常见场景具体见7.1.1. Basic Values of flex

四.布局算法

  1. 生成匿名伸缩项(针对伸缩容器中的文本孩子)

  2. 确定(伸缩)行的长度,分3步:

    1. 确定主轴、交叉轴的可用空间

    2. 确定每个伸缩项的基础尺寸(flex base size)和假定主尺寸(hypothetical main size)

    3. 确定伸缩容器的主尺寸(伸缩项的auto外边距先当成0

  3. 确定主尺寸

    1. 把伸缩项按行排列(1行或多行)

    2. 计算每一项的可伸缩长度

  4. 确定交叉尺寸

    1. 确定每个伸缩项的假定交叉尺寸(hypothetical cross size)

    2. 计算每一行的交叉尺寸

    3. 处理align-content: stretch(让这些伸缩行铺满交叉轴可用空间)

    4. 处理visibility: collapse的伸缩项(这些项主尺寸为0,但仍具有交叉尺寸,即能够影响所在伸缩行的交叉尺寸)

    5. 确定每个伸缩项的交叉尺寸应用值(used cross size)

  5. 处理主轴对齐(逐行为主轴方向具有auto margin的伸缩项分配剩余可用空间,并根据justify-content进行对齐)

  6. 处理交叉轴对齐

    1. 处理交叉轴方向具有auto margin的伸缩项

    2. 逐项按照align-self对齐(针对交叉轴方向不具auto margin的伸缩项)

    3. 确定伸缩容器的交叉尺寸应用值(used cross size)

    4. 所有伸缩行整体根据align-content对齐

P.S.详细布局规则需要考虑各种情况,繁琐复杂,具体见9. Flex Layout Algorithm

计算基础尺寸与假定主尺寸

先不考虑伸缩,进行第一次空间分配(即确定各项的假定主尺寸)

基础尺寸的具体计算步骤如下:

  1. 若设置了确定的flex-basis,就用这个值

  2. 若伸缩项有固有宽高比(intrinsic aspect ratio),并且flex-basis值为content,还有确定的交叉尺寸的话,根据交叉尺寸和宽高比计算

  3. flex-basis值为content,或取决于可用空间,并且伸缩容器有min-contentmax-content约束,就用该伸缩项的最终主尺寸(resulting main size)

  4. 否则,若flex-basis值为content,或取决于可用空间,并且可用主尺寸为无限大,该伸缩项的行内轴还与主轴平行的话,按照writing-mode中的正交流中盒的布局规则来处理,基础尺寸取其最大内容主尺寸(max-content main size),在多语言环境可能出现这种情况

  5. 否则,用flex-basis的应用值代替其主尺寸,并把content当做max-content来计算主尺寸,如果还依赖交叉尺寸,而交叉尺寸为auto或不确定的话,就当其交叉尺寸是fit-content,取其最终主尺寸作为基础尺寸

计算基础尺寸时忽略min/max尺寸限制,假定主尺寸就是加上这个限制后,得到的主尺寸值

计算可伸缩长度(Flexible Length)

伸缩布局,最关键的问题就是如何伸缩(即空间的二次分配,计算各项将因伸缩属性额外获得或失去的空间),步骤如下:

  1. 确定伸缩因子的应用值

    把该行所有伸缩项的假定主尺寸加起来,小于伸缩容器内主尺寸的话,把flex-grow作为伸缩因子,否则就用flex-shrink

  2. 确定不可伸缩项(inflexible item)的尺寸

    把假定主尺寸作为其最终目标主尺寸(target main size),不再变了

    不可伸缩项指的是:伸缩因子值为0的、伸缩因子是flex-grow并且基础尺寸大于假定主尺寸的、伸缩因子是flex-shrink并且基础尺寸小于假定主尺寸的

  3. 计算初始剩余空间

    伸缩容器的内主尺寸,减去该行所有项的外尺寸之和,就是初始剩余空间

    若是不可伸缩项,取其外目标主尺寸,否则取其外基础尺寸

  4. 循环处理

    1. 检查该行每一项,如果所有项都确定了最终目标主尺寸,结束

    2. 计算剩余可用空间作为上面的初始剩余空间

      若未确定最终目标主尺寸的所有项伸缩因子之和小于1,用该值乘以初始剩余空间,如果得到的值的小于剩余可用空间值,就把这个值作为剩余可用空间

    3. 根据伸缩因子按比例分配剩余空间

      1. 剩余可用空间为0,结束

      2. 伸缩因子是flex-grow的话,计算该项的flex-grow值除以该行所有未确定最终目标主尺寸的伸缩项的flex-grow值之和,得到一个比例,目标主尺寸就是基础尺寸加上剩余可用空间中该比例对应的那部分

      3. 伸缩因子是flex-shrink的话,对于该行每一个未确定最终目标主尺寸的伸缩项,用其内基础尺寸乘以其伸缩因子,称为比例比例收缩因子(scaled flex shrink factor)。用该项的比例收缩因子除以该行所有未确定最终目标主尺寸的伸缩项的比例收缩因子,得到一个比例,目标主尺寸就是其基础尺寸减去剩余可用空间中该比例对应部分的绝对值

    4. 处理可伸缩项的min/max限制(如果有的话),把目标主尺寸裁剪到该范围

    5. 处理伸缩过的项

      1. 经上一步裁剪后,如果总尺寸没变(各项需调整差值之和为0),结束

      2. 总尺寸变大了,(上一步中)所有违背min限制的项确定最终目标主尺寸

      3. 总尺寸变小了,所有违背max限制的项确定最终目标主尺寸

    6. 回到循环开始处

  5. 把每一项的主尺寸应用值设置为目标主尺寸

其中,最重要的部分是如何确定拉伸比例与收缩比例(比例相对剩余可用空间),用公式描述如下:

// growFactor   拉伸因子,flex-grow值
// growFactors  该行所有项的拉伸因子
// shrinkFactor 收缩因子,flex-shrink值
// scaledShrinkFactor   比例收缩因子
// scaledShrinkFactors  该行所有项的比例收缩因子
// baseSize     基础尺寸
// freeSpace    剩余可用空间

// 拉伸
growRatio = growFactor / sum(growFactors)
// 收缩
scaledShrinkFactor = baseSize * shrinkFactor
shrinkRatio = scaledShrinkFactor / sum(scaledShrinkFactors)

确定比例之后,修正基础尺寸:

// 拉伸
targetSize = baseSize + growRatio * freeSpace
// 收缩
targetSize = baseSize - abs(shrinkRatio * freeSpace)

例如,最简单的场景:

<div style="display: flex; width: 400px">
    <span style="flex-basis: 200px; flex-shrink: 1; background-color: #ddd">200px</span>
    <span style="flex-basis: 400px; flex-shrink: 1; background-color: #ccc">400px</span>
    <span style="flex-basis: 200px; flex-shrink: 2; background-color: #eee">200px</span>
</div>

呈现效果如下:

| 120px |   240px   |40px|

具体布局过程是这样:

freeSpace = 400 - (200 + 400 + 200) = -400
scaledShrinkFactors = [1 * 200, 1 * 400, 2 * 200]
sum(scaledShrinkFactors) = 1000
shrinkRatios = [0.2, 0.4, 0.4]
targetSizes = [
  200 - abs(0.2 * -400) = 120,
  400 - abs(0.4 * -400) = 240,
  200 - abs(0.4 * -400) = 40
]

当然,这只是最简单的示例场景(伸缩容器可用空间、flex-basis都是固定值),很容器计算基础尺寸、剩余空间及收缩比例,实际应用场景要复杂得多

五.应用场景

  • 按比例布局(几行几列)

  • 对齐控制(横向、纵向居中等)

  • 自适应容器尺寸(铺满或溢出收缩)

这些之前难以实现的场景,在flexbox布局中都很容易搞定。实际上,真正难以驾驭的恰恰是那些之前很容易实现的场景

P.S.为什么非得用felxbox布局?结合使用,各取所长不好吗?因为有些场景没得选,比如RN等基于yoga引擎的CSS环境(只支持flexbox布局)

比如要求icon贴着单行文本的场景,不用flexbox布局的话,可以这样实现:

<div style="width: 100px;">
    <span style="display: inline-block; max-width: 70px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">不长</span><span style="display: inline-block; width: 30px; background-color: #ccc">icon</span>

    <span style="display: inline-block; max-width: 70px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">很长很长很长很长很长很长</span><span style="display: inline-block; width: 30px; background-color: #ccc">icon</span>
</div>

非要用的话,这样做:

<div style="width: 100px; display: flex; flex-wrap: wrap">
    <span style="flex-shrink: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">不长</span><span style="width: 30px; background-color: #ccc">icon</span>

    <div style="max-width: 100%; display: flex">
        <span style="flex-shrink: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">很长很长很长很长很长很长</span><span style="flex: none; width: 30px; background-color: #ccc">icon</span>
    </div>
</div>

关键点在于文本flex-shrink缩回来,这样在文本溢出时能够收缩回来,给icon留出足够的空间,未溢出时,收缩不影响文本宽度,右侧icon就能够紧贴着

另外,第二行容器的max-width: 100%很重要,作为基础尺寸的约束条件。icon的flex: none也很重要,避免假icon的宽度受到挤压(因为flex-shrink默认值是1,空间不足时也会跟着收缩)

参考资料

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code