yoga布局解析

跨端必备技能之flex布局,目前主流的跨端开发方案都使用flex,而其中使用较为广泛的是yoga

前置知识

在开始之前有一些需要知道的前置条件,这些对理解yoga的布局原理非常有帮助。建议想补充完flex相关的知识在继续看yoga部分。

Flex

布局的传统解决方案,基于盒状模型,依赖 display 属性 ,position属性 float属性。它对于那些特殊布局非常不方便,比如,垂直居中就不容易实现。

2009年,W3C 提出了一种新的方案: Flex
可以简便、完整、响应式地实现各种页面布局。目前,它已经得到了所有浏览器的支持。

采用 Flex 布局的元素,称为Flex容器(flex container),简称”容器”。它的所有子元素自动成为容器成员,称为 Flex 项目flex item,简称”项目”。

主轴和侧轴(交叉轴)

容器默认存在两根轴:水平的主轴main axis和垂直的交叉轴cross axis
img

首先每一根轴都包括 三个东西:维度、方向、尺寸。

  • 维度:子项目横着排还是竖着排(x 轴 或 y 轴)。
  • 方向:即排列子元素的顺序 顺序还是逆序。
  • 尺寸:每一个子元素在主轴方向所占的位置的总和。

flex item & flex-basis

flex item:Flex布局下的子元素。
flex-basis:浏览器根据这个属性,计算主轴是否有多余空间。它的默认值为auto,即项目的本来大小。
优先级要高于项目设置的width height。

yoga

YGMeasureMode

当前项目的计算模式。分为3种情况

  1. YGMeasureModeUndefined:
  2. YGMeasureModeAtMost:项目的尺寸不是确切的,但是具有最大值,则取最大值
  3. YGMeasureModeExactly:项目大小的是确定的,可以通过css直接设置,或者经过flex计算得出。

YGNode

对于每一个native控件,都有一个YGNode与之对应,形成绑定关系。同时根据native侧的视图层级结构,来生成yoga侧的node的数据结构。
node主要由4部分组成:

  1. 当前node所在节点树中的信息:父节点,子节点。
  2. css样式。
  3. 布局信息。
  4. 自定函数注入。

YGLayout

YGLayout主要保存了所有运行时计算布局的信息。其中有几个比较重要的属性:

measuredDimensions

每次计算当前项目的尺寸之后,都会把结果存入当前结构。

computedFlexBasis

这个属性十分重要,所有的flex 布局尺寸:拉伸,压缩,换行,都跟这个属性有关。
假设当前项目A有两个子项目。flex-direction 为row。
img
则此时AflexBasis等于B,CflexBasis之和。 而B,C当前的flexBasis由项目本身的内容和css样式决定。

cachedMeasurements

yoga计算布局时,每次都会缓存计算结果,在下一次计算到来时,优先尝试缓存中的所有项,如果不可用在进行布局计算。缓存的内容如下:

1
2
3
4
5
6
7
8
9
struct YGCachedMeasurement {
float availableWidth; //最大有效宽度。一般为父容器宽度
float availableHeight; //高度:同上
YGMeasureMode widthMeasureMode; //本次缓存的计算模式
YGMeasureMode heightMeasureMode; //同上

float computedWidth; //经过计算之后的计算宽度
float computedHeight; //同上
}

YGStyle

YGStyle主要保存了开发者自己设置的css样式。

下面是上面三者的关系。
img

布局流程

img

由于源码过长,笔者将注释过的源码放到了这里

踩坑

文本显示不全(float精度失真问题)

yoga 在处理文本的节点时,采用了下面的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

static void YGRoundToPixelGrid(
const YGNodeRef node,
const float pointScaleFactor,
const float absoluteLeft,
const float absoluteTop) {
if (pointScaleFactor == 0.0f) {
return;
}

const float nodeLeft = node->getLayout().position[YGEdgeLeft];
const float nodeTop = node->getLayout().position[YGEdgeTop];

const float nodeWidth = node->getLayout().dimensions[YGDimensionWidth];
const float nodeHeight = node->getLayout().dimensions[YGDimensionHeight];

const float absoluteNodeLeft = absoluteLeft + nodeLeft;
const float absoluteNodeTop = absoluteTop + nodeTop;

const float absoluteNodeRight = absoluteNodeLeft + nodeWidth;
const float absoluteNodeBottom = absoluteNodeTop + nodeHeight;

// If a node has a custom measure function we never want to round down its
// size as this could lead to unwanted text truncation.
const bool textRounding = node->getNodeType() == YGNodeTypeText;

node->setLayoutPosition(
YGRoundValueToPixelGrid(nodeLeft, pointScaleFactor, false, textRounding),
YGEdgeLeft);

node->setLayoutPosition(
YGRoundValueToPixelGrid(nodeTop, pointScaleFactor, false, textRounding),
YGEdgeTop);

// We multiply dimension by scale factor and if the result is close to the
// whole number, we don't have any fraction To verify if the result is close
// to whole number we want to check both floor and ceil numbers
float mod = fmodf(nodeWidth * pointScaleFactor, 1.0);

//是否高度或者宽度不是整数,如:3.8,3.1 这样
const bool hasFractionalWidth =
!YGFloatsEqual(mod, 0) &&
!YGFloatsEqual(mod, 1.0);
const bool hasFractionalHeight =
!YGFloatsEqual(fmodf(nodeHeight * pointScaleFactor, 1.0), 0) &&
!YGFloatsEqual(fmodf(nodeHeight * pointScaleFactor, 1.0), 1.0);
//当宽度不是整数(有余数)时。对于文本的内容应该向上取整。
//但由于YGFloatsEqual内部,在float比较时,精度为0.0001f。导致某些极端的情况下,文本内容被截断
//如:文本宽度刚好为 3.0000999,此时会被认定为3,导致文本显示不全。
node->setLayoutDimension(
YGRoundValueToPixelGrid(
absoluteNodeRight,
pointScaleFactor,
(textRounding && hasFractionalWidth),
(textRounding && !hasFractionalWidth)) -
YGRoundValueToPixelGrid(
absoluteNodeLeft, pointScaleFactor, false, textRounding),
YGDimensionWidth);

node->setLayoutDimension(
YGRoundValueToPixelGrid(
absoluteNodeBottom,
pointScaleFactor,
(textRounding && hasFractionalHeight),
(textRounding && !hasFractionalHeight)) -
YGRoundValueToPixelGrid(
absoluteNodeTop, pointScaleFactor, false, textRounding),
YGDimensionHeight);

const uint32_t childCount = YGNodeGetChildCount(node);
for (uint32_t i = 0; i < childCount; i++) {
YGRoundToPixelGrid(
YGNodeGetChild(node, i),
pointScaleFactor,
absoluteNodeLeft,
absoluteNodeTop);
}
}

造成这样的原因是:
当宽度不是整数(有余数)时。对于文本的内容应该向上取整。
但由于YGFloatsEqual内部,在float比较时,精度为0.0001f。导致某些极端的情况下,文本内容被截断。
如:文本宽度刚好为 3.0000999,此时会被认定为3,导致文本显示不全。

解决办法:
参考rn

1
2
3
4
5
6
7
// Adding epsilon value illuminates problems with converting values from
// `double` to `float`, and then rounding them to pixel grid in Yoga.
CGFloat epsilon = 0.001;
return (YGSize){
RCTYogaFloatFromCoreGraphicsFloat(size.width + epsilon),
RCTYogaFloatFromCoreGraphicsFloat(size.height + epsilon)
};

在注入的计算函数中,强制向上取整。

参考文献

flex
yoga