[源码分析] - 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 布局表面上类似于块布局. 它不提供在块布局中以文本或文档为中心的复杂属性,例如floatscolumns. 它以一种简单而强大的工具,能够描述如何在flex box中分配空间和对齐内容. 它的内容包括:

  • flow direction支持以任意方向排版(左右, 上下)
  • 支持order 逆序或元素的任意排布(视觉顺序不影响代码的书写顺序)
  • 可以在主轴main axis上顺序单行排布, 也可以wrap 在次轴cross axis上根据长度折为多行
  • 可以在动态空间时flex伸缩它的大小
  • 可以定义元素在主轴,次轴上的对齐align方式
  • 可以在主轴上动态折叠collapse, 且不损失次轴大小

flex box 的声明与空间划分

image.png

声明了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, 会有三种方式去计算.
    1. specified size suggestion:  用户声明的主轴大小
      2. transferred size suggestion: 如果元素的纵横比固定, 且拥有次轴大小, 则使用次轴大小计算
      3. content size suggestion: 纵横比+次轴大小转换, 是主轴的最小大小

Ordering and Orientation: 元素的顺序与朝向

flex 引入的新属性来定义排版的顺序与朝向, floatclear 在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 + wrap
  • order: 整数值
    自定义元素在容器中被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 是独立计算的

image.png

此图中, 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, 空间全部通过空余空间基于数值分配.

image.png

此图中, 三个元素的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: 如何在主轴对齐, 一图带过,

image.png

space-between, space-around, space-evenly(文档中没有提到) 的区别主要在, 开头结尾的space如何计算, between 没有 space, around 共用一个, evenly 开头结尾分别计算

  • align-items, align-self: 如何在纵轴对齐 同一个属性, 区别是作用于container还是item

image.png

stretch的优先级为: 定长 > auto margin + 最大最小约束 > stretch

  • align-content: multi-line 版的 align-items,

image.png
这里标准特意说明了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
  • 分配空间:
    1. 主轴方向的行内(初始, 自由)空间如何分配? 基于什么比例? => flex = grow + shrink + basis(基准大小)
    2. 如何对齐, 既如何自由空间分配到元素的间距上? =>
      • auto margin
      • justify-content : 主轴分配
      • align-items, align-self, align-content: 次轴分配

flex 算法

前文我们关注的是flex的属性, 理解了它的概念, 但css属性是声明式的, 它不能反映排版引擎如何得到正确的结果. 文档在第九章提出了一个算法来实现布局. 注意这个算法不是标准的一部分. 我们以该算法结合它的一个rust的实现taffy, 来分析.

  • 初始化
  1. 将html元素转换为flex items, 如上文 #flex items 所述
  • 确定line的长度
  1. 确定flex box的主轴,次轴的可用空间, 顺序如下
    • 已经定长(definite size)
    • min-max constraint
    • flex 容器本身的长度 - margin, border, padding (可能是一个无穷值)
  2. 确定每个item的基准大小和假设主轴大小, 首先是基准(basis)大小
    • 已经定长
    • 固定aspect ratio, 尝试使用侧轴大小
    • 测量元素(这里补充一下, 任何一个html元素理论上都是可测量的, 最典型的就是文字元素, 我们不需要指定它的长度就可以在文档中正确展示, 但如果用户显式设置了其大小, 则用户设置值优先级更高)
      计算基准大小时会忽略元素的min-max constraint, 考虑上min-max constraint 的大小为假设主轴大小
  3. 计算flex box的主轴大小
  • 确定元素的主轴大小
  1. 将元素拆分进flex lines.
  2. 解析元素的flexible长度作为他们的主轴长度
  • 确定元素的侧轴大小
  1. 确定元素的假设侧轴大小: 利用主轴大小与可用空间进行一次layout
  2. 计算每个flex line的侧轴大小
  3. 处理align-content:stretch
  4. 处理visibility:collapse
  5. 确定元素的侧轴大小
  • 确定主轴对齐
  1. 分配自由空间: 先 auto margin, 再 justify-content
  • 确定侧轴对齐
  1. 侧轴auto margin
  2. 使用align-self对齐item
  3. 确定flex box的侧轴大小
  4. 使用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中的步骤如下:

  1. 首先确定可用空间, 它表示了节点可以占用的最大空间
  2. 其次对两个轴分别先计算假设大小, 在计算实际大小. 分两步计算可以解决依赖关系.
  3. 在分完flex line之后, 我们已经可以确定每个节点在flex box中的二维相对位置, 后续需要分配自由空间
  4. 分配自由空间的规则可分为三类, 1. flex定义的增加大小规则(flex factor, stretch), 2. auto margin, 3. margin 分配(justify content, align items), 它们之间存在着优先级.

相关连接

flex 的实现有很多, 本文采用最贴近文档描述的rust实现分析, 其他的还有各大浏览器(blink), react native的排版引擎 yoga, 同时他还自带了一个playground, 但需要主要非浏览器环境下的实现可能标准支持不完整, 浏览器环境的支持可见 Can I use.

相关文章

posted @ 2024-01-15 10:51  新新人類  阅读(53)  评论(0编辑  收藏  举报