[源码分析] - flex 标准文档导读与 一个rust实现解析
本文是w3中css-flexbox[标准文档](CSS Flexible Box Layout Module Level 1 (w3.org)解读. (2023.1), 并对一些开源实现进行调研分析.
文档导读
css layout mode
css layout 模式用于确定在盒模型中的元素如何排布(大小与位置), 在css 2.1中有如下几种方式.
- block layout, 块级别, 文档的默认排版方式
- inline layout, 行级别, 文本的排版方式
- table layout, 二维表格形式
- positioned layout, 绝对布局
在新的css标准中, 提出了flex与grid排版, 它们功能灵活强大, 且为一些排版方式提供的标准化语义.
Flex 布局表面上类似于块布局. 它不提供在块布局中以文本或文档为中心的复杂属性,例如floats
和columns
. 它以一种简单而强大的工具,能够描述如何在flex box中分配空间和对齐内容. 它的内容包括:
flow direction
支持以任意方向排版(左右, 上下)- 支持
order
逆序或元素的任意排布(视觉顺序不影响代码的书写顺序) - 可以在主轴
main axis
上顺序单行排布, 也可以wrap
在次轴cross axis
上根据长度折为多行 - 可以在动态空间时
flex
伸缩它的大小 - 可以定义元素在主轴,次轴上的对齐
align
方式 - 可以在主轴上动态折叠
collapse
, 且不损失次轴大小
flex box 的声明与空间划分
声明了display: flex | inline-flex
的元素就会成为flex box(container), 并对其中的flex item元素应用flex布局算法.
css 盒模型处于2d平面上, flex box 将其扩展为主轴与横轴, 所有元素在主轴空间上进行布局
flex items: flex 中的元素
flex box中每一个需要参与排版, 占据空间的元素都对应一个flex item
- html 元素 (display:none 除外)
- 有内容的匿名文本块
以下是文档规定的一些css属性对flex item的生效方式.
- absolutely-positioned(绝对布局): 排版时被当做flex box中唯一的item, 有些属性可以生效(align-self), 有些不会(stretch)
- margin/padding
- z-ordering: stack 的方式堆叠
- visibility:collapse : 有点类似display:none, 但可能会导致重复运行layout算法
- automatic minimum size(自动最小大小):
当主轴的 min-width/height 属性设置了auto, 应该如何计算.
滑动容器主轴的自动最小大小是0.
对非滑动容器的主轴上的自动最小大小是 content-based minimum size, 会有三种方式去计算.- specified size suggestion: 用户声明的主轴大小
2. transferred size suggestion: 如果元素的纵横比固定, 且拥有次轴大小, 则使用次轴大小计算
3. content size suggestion: 纵横比+次轴大小转换, 是主轴的最小大小
- specified size suggestion: 用户声明的主轴大小
Ordering and Orientation: 元素的顺序与朝向
flex 引入的新属性来定义排版的顺序与朝向, float
和 clear
在flex box 中 不再起作用.
flex-direction
: row<-reverse> | column<-reverse>
基于[writing-mode(书写方向)]((https://www.w3.org/TR/css-writing-modes-4/#writing-mode) 设定主轴方向, 既属性为row时与书写方向平行, 为column时与书写方向平行,flex-wrap
: nowrap | wrap<-reverse>
是否折行, 折行规则类似于文字排版flex-flow
: direction + wraporder
: 整数值
自定义元素在容器中被layout, 和paint的顺序, 基于order-modified document order, 先看order, 同order则根据文档中顺序. 绝对布局元素被视作 order:0(只生效paint order), order 可以为负数, 默认是0
reorder and accessibility: order 只应该影响 layout 与 paint 顺序, 不影响逻辑顺序(tabindex等)
Flex line
折行后flex box 中的每行被当做一个flex line.
flex line叠加在次轴上(平行主轴), 行内元素的flexible length 与 justify content/align-self 是独立计算的
此图中, flex box在layout 元素1,2,3之后剩余的空间不足以放置元素4, 因此发生折行, 元素1,2,3构成一个flex line, 元素4 单独为一个flex line.
Flexibility : 可伸缩性
基于声明在flex item上的 flex属性及三个子属性来提供 flexible length.
它通过 flex factors(grow/shrink)和 flex basis 来控制元素在有空余空间时增长, 空间不足时缩放的行为, 既在主轴上更灵活的控制自身大小.
flex-grow
: space = free space / sum(grow factor of all peer items) * grow factor. 特别的, 如果所有grow factor的和小于1, sum(grow factor) 会当做1来算, 因此更推荐使用整数flex-shrink
: 类似grow, 不过shrink是free space 为负数时如何缩小.(默认情况下不会缩小到 minimum content size)flex-basis
: 在基于flex factors分配自由空间前, 元素在主轴上的基础值大小, 取值可以为- auto : 取item在主轴上的大小
- content: 基于内容自动计算, 通常等于 css max-content size, 但会加上额外调整
- <width>: 默认的css长度算法.
flex
属性集合了上述三个子属性, 它提供了默认值, 避免错误情况, 更推荐使用.
- initial: 默认值, 0 1 auto
- auto: 1 1 auto
- none: 0 0 auto (grow, shrink 都没有的情况叫做inflexible)
- 非负数值: 数值 1 0, 默认没有basis, 空间全部通过空余空间基于数值分配.
此图中, 三个元素的growth分别为1 1 2, 如果basis都为0, 则所有空间都为自由空间, 因此三个元素的宽度比也为1:1:2, 如果设置了basis为auto(基于内容的宽度)
Alignment: 如何对齐
-
auto margins: 类似于block中的auto margin
优先级: flex basis + flexibile lengths(上一节) > auto margin > justify-content/align-self. 高优先级的属性会先一步将自由空间分配掉, 后续属性就不起作用.
overflowing (长度已经超过主轴, 且没有wrap折行)的元素会忽略它们end方向的auto margin与overflow -
justify-content
: 如何在主轴对齐, 一图带过,
space-between, space-around, space-evenly(文档中没有提到) 的区别主要在, 开头结尾的space如何计算, between 没有 space, around 共用一个, evenly 开头结尾分别计算
align-items
,align-self
: 如何在纵轴对齐 同一个属性, 区别是作用于container还是item
stretch的优先级为: 定长 > auto margin + 最大最小约束 > stretch
align-content
: multi-line 版的align-items
,
这里标准特意说明了align-content只在multi-line起作用, single line 时 free space 全部分配给单独的line上
但其实有个小问题没说, align-items在multi-line中如何起作用? 我认为可以将所有line看作一个line, 然后做align-items的对齐
Flex Container Baselines: flex box 如何在两个轴去找(first, last)baseline, 更多关于baseline 请看 CSS Writing Modes 3 §4.1 Introduction to Baselines (w3.org) 和 CSS Writing Modes 3 §4.1 Introduction to Baselines (w3.org)
总结
flex 的排版以灵活著称, 我认为它把握了关键要素: 划分空间, 相对于传统css布局和注重于元素之间关系的相对布局, 加强了对空间的掌控能力.
针对于如何划分空间这个大问题, 可以分成不同的小问题, 并使用不同属性来满足需求.
- 规划空间:
- 以什么方向排版? => direction 确定主轴,次轴
- 允许元素修改自己的排版顺序 => order
- 内容过长是否折行? => wrap, flex line
- 分配空间:
- 主轴方向的行内(初始, 自由)空间如何分配? 基于什么比例? => flex = grow + shrink + basis(基准大小)
- 如何对齐, 既如何自由空间分配到元素的间距上? =>
- auto margin
- justify-content : 主轴分配
- align-items, align-self, align-content: 次轴分配
flex 算法
前文我们关注的是flex的属性, 理解了它的概念, 但css属性是声明式的, 它不能反映排版引擎如何得到正确的结果. 文档在第九章提出了一个算法来实现布局. 注意这个算法不是标准的一部分. 我们以该算法结合它的一个rust的实现taffy, 来分析.
- 初始化
- 将html元素转换为flex items, 如上文 #flex items 所述
- 确定line的长度
- 确定flex box的主轴,次轴的可用空间, 顺序如下
- 已经定长(definite size)
- min-max constraint
- flex 容器本身的长度 - margin, border, padding (可能是一个无穷值)
- 确定每个item的基准大小和假设主轴大小, 首先是基准(basis)大小
- 已经定长
- 固定aspect ratio, 尝试使用侧轴大小
- 测量元素(这里补充一下, 任何一个html元素理论上都是可测量的, 最典型的就是文字元素, 我们不需要指定它的长度就可以在文档中正确展示, 但如果用户显式设置了其大小, 则用户设置值优先级更高)
计算基准大小时会忽略元素的min-max constraint, 考虑上min-max constraint 的大小为假设主轴大小
- 计算flex box的主轴大小
- 确定元素的主轴大小
- 将元素拆分进flex lines.
- 解析元素的flexible长度作为他们的主轴长度
- 确定元素的侧轴大小
- 确定元素的假设侧轴大小: 利用主轴大小与可用空间进行一次layout
- 计算每个flex line的侧轴大小
- 处理
align-content:stretch
- 处理
visibility:collapse
- 确定元素的侧轴大小
- 确定主轴对齐
- 分配自由空间: 先 auto margin, 再 justify-content
- 确定侧轴对齐
- 侧轴auto margin
- 使用
align-self
对齐item - 确定flex box的侧轴大小
- 使用
align-content
对齐lines
/// Compute a preliminary size for an item
fn compute_preliminary(
tree: &mut impl LayoutTree, // 全局的节点树, 可以根据节点获取: 属性, 子节点等信息.
node: Node,
known_dimensions: Size<Option<f32>>, // 当前节点已知的宽高
parent_size: Size<Option<f32>>, // 父节点已知的宽高
available_space: Size<AvailableSpace>,// layout的可用空间
run_mode: RunMode,
) -> Size<f32> {
// Define some general constants we will need for the remainder of the algorithm.
let mut constants = compute_constants(tree.style(node), known_dimensions, parent_size);
// 9. Flex Layout Algorithm
// 9.1. Initial Setup
// 1. Generate anonymous flex items as described in §4 Flex Items.
// 生成flex items
let mut flex_items = generate_anonymous_flex_items(tree, node, &constants);
// 9.2. Line Length Determination
// 2. Determine the available main and cross space for the flex items
// 确定可用空间
let available_space = determine_available_space(known_dimensions, available_space, &constants);
let has_baseline_child = flex_items.iter().any(|child| child.align_self == AlignSelf::Baseline);
// 3. Determine the flex base size and hypothetical main size of each item.
// 确定item的基准大小
determine_flex_base_size(tree, known_dimensions, &constants, available_space, &mut flex_items);
// TODO: Add step 4 according to spec: https://www.w3.org/TR/css-flexbox-1/#algo-main-container
// 标准中的第4步, 确定flex container的主轴大小, 但并不容易计算,
// 因此实现选择在6步之后的TODO中, 根据最长的flex line计算.
// 9.3. Main Size Determination
// 5. Collect flex items into flex lines.
// 拆分flex lins, 这里还没有确定主轴长度, 因此最大长度选的是可用空间的大小.
let mut flex_lines = collect_flex_lines(tree, node, &constants, available_space, &mut flex_items);
// If container size is undefined, re-resolve gap based on resolved base sizes
// gap 是每个元素之间的间距.
let original_gap = constants.gap;
if constants.node_inner_size.main(constants.dir).is_none() {
let longest_line_length = flex_lines.iter().fold(f32::MIN, |acc, line| {
let length: f32 = line.items.iter().map(|item| item.hypothetical_outer_size.main(constants.dir)).sum();
acc.max(length)
});
let style = tree.style(node);
let new_gap = style.gap.main(constants.dir).maybe_resolve(longest_line_length).unwrap_or(0.0);
constants.gap.set_main(constants.dir, new_gap);
}
// 6. Resolve the flexible lengths of all the flex items to find their used main size.
// 解析每个元素分配完自由空间的主轴大小
// 该算法需要反复迭代, 解决flex factor 与 target size 不一致的问题.
for line in &mut flex_lines {
resolve_flexible_lengths(tree, line, &constants, original_gap);
}
// TODO: Cleanup and make according to spec
// Not part of the spec from what i can see but seems correct
// 根据flex line 计算container主轴长度
constants.container_size.set_main(
constants.dir,
known_dimensions.main(constants.dir).unwrap_or({
let longest_line =
flex_lines.iter().fold(f32::MIN, |acc, line| acc.max(line.container_main_size_contribution));
let size = longest_line + constants.padding_border.main_axis_sum(constants.dir);
match available_space.main(constants.dir) {
AvailableSpace::Definite(val) if flex_lines.len() > 1 && size < val => val,
_ => size,
}
}),
);
// inner size 要去掉padding和border
constants.inner_container_size.set_main(
constants.dir,
constants.container_size.main(constants.dir) - constants.padding_border.main_axis_sum(constants.dir),
);
// 9.4. Cross Size Determination
// 7. Determine the hypothetical cross size of each item.
// 计算item在侧轴的假设大小
for line in &mut flex_lines {
determine_hypothetical_cross_size(tree, line, &constants, available_space);
}
// TODO - probably should move this somewhere else as it doesn't make a ton of sense here but we need it below
// TODO - This is expensive and should only be done if we really require a baseline. aka, make it lazy
// 递归计算每个子节点的baseline
if has_baseline_child {
calculate_children_base_lines(tree, node, known_dimensions, available_space, &mut flex_lines, &constants);
}
// 8. Calculate the cross size of each flex line.
// 计算每个flex line 的横轴大小
calculate_cross_size(tree, &mut flex_lines, known_dimensions, &constants);
// 9. Handle 'align-content: stretch'.
handle_align_content_stretch(tree, &mut flex_lines, node, known_dimensions, &constants);
// 10. Collapse visibility:collapse items.
// TODO implement once (if ever) we support visibility:collapse
// visibility: collapse 暂未支持
// 11. Determine the used cross size of each flex item.
// 确定item在侧轴的大小(align-self:stretch / 假设侧轴大小)
determine_used_cross_size(tree, &mut flex_lines, &constants);
// 9.5. Main-Axis Alignment
// 12. Distribute any remaining free space.
// 主轴对齐
distribute_remaining_free_space(tree, &mut flex_lines, node, &constants);
// 9.6. Cross-Axis Alignment
// 13. Resolve cross-axis auto margins (also includes 14).
// 侧轴margin和对齐
resolve_cross_axis_auto_margins(tree, &mut flex_lines, &constants);
// 15. Determine the flex container’s used cross size.
// flex container使用了的侧轴大小
let total_line_cross_size = determine_container_cross_size(&mut flex_lines, known_dimensions, &mut constants);
// We have the container size.
// If our caller does not care about performing layout we are done now.
// 如果只是计算大小, 则直接返回, 否则继续layout子节点
if run_mode == RunMode::ComputeSize {
let container_size = constants.container_size;
return container_size;
}
// 16. Align all flex lines per align-content.
// 根据align-content, 计算multi line的间距.
align_flex_lines_per_align_content(tree, &mut flex_lines, node, &constants, total_line_cross_size);
// Do a final layout pass and gather the resulting layouts
// 遍历 layout 子节点, orientation中的reverse在此生效.
final_layout_pass(tree, node, &mut flex_lines, &constants);
// 绝对布局, display:none 的子节点
perform_absolute_layout_on_absolute_children(tree, node, &constants);
layout_display_none_children(tree, node);
// 返回主轴大小
constants.container_size
}
css排版的计算是一个很复杂的问题, 需要考虑很多相互依赖的不同因素, 计算的时候可能既需要自上而下, 也需要自下而上. 但整体的思路还是由已知推未知, 先从不受影响的因素开始计算.
flex box中的步骤如下:
- 首先确定可用空间, 它表示了节点可以占用的最大空间
- 其次对两个轴分别先计算假设大小, 在计算实际大小. 分两步计算可以解决依赖关系.
- 在分完flex line之后, 我们已经可以确定每个节点在flex box中的二维相对位置, 后续需要分配自由空间
- 分配自由空间的规则可分为三类, 1. flex定义的增加大小规则(flex factor, stretch), 2. auto margin, 3. margin 分配(justify content, align items), 它们之间存在着优先级.
相关连接
flex 的实现有很多, 本文采用最贴近文档描述的rust实现分析, 其他的还有各大浏览器(blink), react native的排版引擎 yoga, 同时他还自带了一个playground, 但需要主要非浏览器环境下的实现可能标准支持不完整, 浏览器环境的支持可见 Can I use.
相关文章