专栏名称: SegmentFault思否
SegmentFault (www.sf.gg)开发者社区,是中国年轻开发者喜爱的极客社区,我们为开发者提供最纯粹的技术交流和分享平台。
目录
相关文章推荐
OSC开源社区  ·  2024: 大模型背景下知识图谱的理性回归 ·  2 天前  
OSC开源社区  ·  升级到Svelte ... ·  3 天前  
程序猿  ·  “未来 3 年内,Python 在 AI ... ·  3 天前  
程序员的那些事  ·  惊!小偷“零元购”后竟向 DeepSeek ... ·  2 天前  
51好读  ›  专栏  ›  SegmentFault思否

LayoutUnit & Subpixel Layout

SegmentFault思否  · 公众号  · 程序员  · 2020-02-10 12:01

正文

本文转载于思否社区专栏:Jiavan

作者:Jiavan



No.1

引言



为了更好的支持移动端和 PC 端的缩放,WebKit 增加了subpixel layout (次像素/亚像素布局) 为此他们还改变了 rendering tree。一个次像素单元在 WebKit 内被称为 LayoutUnit 用于取代之前使用整数来布局一个元素在页面中位置和大小。从 2013 年开始 WebKit 就已经开启了这个 flag。


No.2

LayoutUnit



LayoutUnit 是逻辑像素的一种抽象表示,在 WebKit 的实现中它是一个像素的 1/64,这样我们就可以使用整数来进行布局计算,避免了使用浮点数计算而丢失精度的问题。

虽然我们现在在布局计算时使用了 LayoutUnit,但是在最终将计算值渲染对应到设备上时仍然会出现计算值不能与物理像素对齐的情况。因为计算出的值可能是一个小数而 1 个物理像素已经不能再进行切割。所以出现了这样一个问题,次像素如何与物理像素进行对齐?

回到我们实际的编程过程中,我们会有很多场景遇到次像素的问题,只是很多人不会关注,或者会忽略掉这些细节。比如如果一个 box 的宽度是 10px,我们把它平均分成 3 份,那么里面的三个盒子的宽度分别是多少呢,3.3333px?再比如我们在使用 rem 布局的时候有时候会发现一个正方形设置了 border-raduis 预期让它展示成一个圆形,在一些设备上却并不那么圆,在整体比较小的时候可能会被渲染成一个椭圆形。以及这种时候这个元素还设置了一个 background-size 覆盖整个容器但是背景却被切掉了一小块。这些问题不是那么容易被发现,但是确实是存在的。


No.3

场景



现在我们有一个 50px 的容器 (DPR 为 1) 将他分成 3 份,必然会出现小数的情况,看看每一份渲染在屏幕上的宽度是多少。
class="container">
style="background: #111">

style="background: #222">

style="background: #333">

.container {
display: flex;
width: 50px;
height: 30px;
background: #999;
}
.container div {
flex: 1;
height: 100%;
}
const getWidth = () => {
const container = document.querySelector('.container');
const nodes = Array.prototype.slice.call(container.children);
nodes.forEach((i, index) => {
console.log(
`${index} width: ${i.clientWidth}, computed width: ${
i.getBoundingClientRect().width
}
`

);
});
};
getWidth();
// console
0 width: 17, computed width: 16.671875
1 width: 16, computed width: 16.671875
2 width: 17, computed width: 16.671875
我们发现三份的 clientWidth 并不是一样的,其中一个会少一像素,但是它们的宽度加起来仍然是容器的宽度。而通过 getBoundingClientRect 获得的计算值却是一样的,但是并不像我们预期的一样是 50/3 = 16.666666667,而是 16.671875 看起来也并没有什么四舍五入的关系。但是从上面的例子中我们可以得到的一个结论就是,上面三份中的宽度最终在屏幕上并不是完全一致的,这也会导致我们在其他场景下遇到类似的问题,比如说在一个页面中同一个组件渲染出来的元素在页面的多个位置上可能表现出不一致的情况,有些元素可能渲染出来会多 1px 或者少 1px,在像素越小的地方对比度就会越明显,比如一个高度是 3px,另一个 2px,这样就会看出明显的差异。而如果一个是 100px 另一个是 101px,你可能就没有感知了。

上面还有一个问题没有解决,就是计算值和我们预期不一致。这里就可以通过 LayoutUnit 来解释。上面我们提到在布局的使用会使用 subpixel layout 把一个像素分成 64 份。这样我们看看 WebKit 在布局的时候是怎么就算的:
1. container width: 50px * 64 => 3200
2. 每一个子 div: 3200 / 3 = round(1066.666666667) => 1067
3. 最终计算值: 1067 / 64 => 16.671875
通过上面的计算我们发现结果和 getBoundingClientRect 获得的值完全吻合,所以这里计算元素大小的时候浏览器内核使用了 subpixel layout,而不是直接使用原来的 pixels。

这里仍然面临了另一个问题,我们使用 subpixel layout 计算出来的值仍然是一个小数,但是我们布局的时候是如何和物理像素进行对齐的呢?

上面少掉 1px 的元素仅是因为把 getBoundingClientRect 的值进行四舍五入?那这样也应该全是 17px,而单单中间的一个元素少了一像素?


No.4

如何对齐



在进行 subpixel 和 pixel 之间转换时,有两种方式,一种是 enclosingIntRect 另一种是 pixelSnappedIntRect 在上述的例子中使用了第二种转换方式。


上面的图中,灰色格子代表物理像素,蓝色区域表示 subpixel layout 计算值,黑色区域表示最终 subpixel -> pixel 的对齐结果。

enclosingIntRect 算法:

x: floor(x)
y: floor(y)
maxX: ceil(x + width)
maxY: ceil(y + height)
width: ceil(x + width) - floor(x)
height: ceil(y + height) - floor(y)
这种计算方式很简单,直接选择最小的完全能覆盖住计算结果的物理像素区域。

pixelSnappedIntRect 算法:
y: round(y)
maxX: round(x + width)
maxY: round(y + height)
width: round(x + width) - round(x)
height: round(y + height) - round(y)
pixelSnappedIntRect 的计算也很简单,它直接 round 到离自己最近的一个物理像素。

接着上面的例子,我们现在把 50px 分层 6 份来模拟计算下看看每一份的宽度计算值应该是多少:
1. container width: 50px * 64 => 3200
2. 每一个子 div: 3200 / 6 = round(533.333333333) => 533
3. 最终计算值: 533 / 64 => 8.328125
// log
0 width: 8, computed width: 8.328125
1 width: 9, computed width: 8.328125
2 width: 8, computed width: 8.328125
3 width: 8, computed width: 8.328125
4 width: 9, computed width: 8.328125
5 width: 8, computed width: 8.328125
看到 js 算出来的值和我们算出来的是一致的,并不简单的是 50/6 = 8.333333333。在最终渲染的时候:

第一个元素 :直接从容器左边开始绘制,发现 8.328125 多余的小数无法解决直接 round 到最近物理像素,得到 8px 绘制空间,但是在逻辑空间上第一个元素占用了第 9 个像素 0.328125px 空间,为了和物理像素对齐,下一个元素应该在绘制时加上这块空间。

第二个元 :8 + 8.328125 + .328125 => 16.65625 => round(16.65625) => 17,这里第二个元素加第一个元素宽度应该是 17px 所以第二个元素宽度是 9px 而不是 8px,这里其实两个元素加起来还不足 17px,由于对齐规则四舍五入,让第二个元素直接到 17px,在第三个元素绘制时其实左边还有 17 - 16.65625 => 0.34375px 的逻辑空间。

第三个元素 :由于左边还有剩余的逻辑空间,17 + 8.328125 - 0.34375 => 24.984375 => round(24.984375) => 25。此时宽度来到了 25,减去之前第一二个元素的宽度 17,得到第三个元素宽度为 8px。

第四个元素 :按照上述规则就不具体说明,25 + 8.328125 - (25 - 24.984375) => 33.3125 => round(33.3125) => 33;33 - 25 => 8px。

第五个元 :33 + 8.328125 + 0.3125 => 41.640625 => round(41.640625) => 42;42 - 33 => 9px。

第六个元素 :剩余 50-41.640625 => 8.359375,对齐到最近的空间剩余 8px。

和上述 js 获取的 clientWidth 结果 8,9,8,8,9,8 完全一致。所以这里元素的大小可以通过 pixelSnappedIntRect 对齐方式来解释为什么有些元素会多/少一像素并且出现是「没有规则」的。


No.5

如何选择







请到「今天看啥」查看全文