当前位置: 首页 > Web前端 > CSS

LayoutUnit&SubpixelLayout

时间:2023-03-30 19:26:41 CSS

LayoutUnit&SubpixelLayout简介为了更好的支持移动端和PC端的缩放,WebKit加入了subpixel布局(sub-pixel/sub-pixellayout)。为此,他们还更改了渲染树。WebKit中称为LayoutUnit的子像素单元取代了以前使用整数来布局元素在页面上的位置和大小。WebKit自2013年起启用此标志。LayoutUnitLayoutUnit是逻辑像素的抽象表示。在WebKit的实现中,它是一个像素的1/64,这样我们就可以使用整数来进行布局计算,避免使用浮点数计算时丢失精度的问题。虽然我们现在在布局计算中使用了LayoutUnit,但是当计算出的值最终渲染出来并对应到设备上时,仍然会出现计算出的值无法与物理像素对齐的情况。因为计算出来的值可能是小数,1个物理像素就不能再切了。那么问题来了,子像素如何与物理像素对齐?回到我们实际的编程过程中,我们在很多场景中都会遇到亚像素的问题,但是很多人不会去关注,或者会忽略这些细节。比如一个盒子的宽度是10px,我们把它分成3份,那么里面的三个盒子的宽度是多少,3.3333px?再比如,我们在使用rem布局的时候,有时候会发现一个正方形设置了border-raduis,期望显示成圆形,但是在某些设备上却不是那么圆,可能会渲染成一个整体当它相对较小时呈椭圆形。而这个时候这个元素也设置了一个background-size来覆盖整个容器,只是背景被裁掉了一小块。这些问题并不那么容易发现,但它们确实存在。场景现在我们有一个50px的容器(DPR为1)并将它分成3部分。必须有小数。查看屏幕上呈现的每个部分的宽度有多少。

.container{显示:flex;宽度:50px;高度:30px;背景:#999;}.containerdiv{flex:1;height:100%;}constgetWidth=()=>{constcontainer=document.querySelector('.container');constnodes=Array.prototype.slice.call(container.children);nodes.forEach((i,index)=>{console.log(`${index}宽度:${i.clientWidth},计算宽度:${i.getBoundingClientRect().width}`);});};getWidth();//console0width:17,computedwidth:16.6718751width:16,computedwidth:16.6718752width:17,computedwidth:16.671875我们发现三份的clientWidth不一样,其中一份会少一个像素,但它们的组合宽度仍然是容器的宽度。通过getBoundingClientRect得到的计算值是一样的,但是不是我们预期的50/3=16.666666667,而是16.671875好像没有任何舍入关系。但是从上面的例子我们可以得出一个结论就是上面三个副本的宽度在屏幕上并不完全一样,这也会导致我们在其他场景下也会遇到类似的问题,比如在一个页面上渲染的元素页面中的同一个组件可能在页面的多个位置显示不一致。某些元素可能会渲染多1px或少1px。像素越小,对比度越明显。例如,一个高度是3px,另一个是2px,您会看到明显的差异。而如果一个是100px,另一个是101px,你可能察觉不到。上面还有一个问题没有解决,就是计算出来的值和我们的预期不一致。这里可以用LayoutUnit来解释。上面我们提到layout的使用会使用subpixellayout将一个pixel分成64份。这样,我们看看WebKit在布局时是如何计算的:1.容器宽度:50px*64=>32002每个子div:3200/3=round(1066.666666667)=>10673最终计算值:1067/64=>16.671875通过上面的计算,我们发现结果和getBoundingClientRect得到的值是完全一样的,所以这里计算元素大小时,浏览器内核使用的是亚像素布局,而不是直接使用原始像素。这里还有一个问题。subpixellayout计算出来的值还是一个小数,但是我们layout的时候怎么和物理像素对齐呢?上面的1px元素是否只是因为getBoundingClientRect的值四舍五入而丢失?所以应该都是17px,只有中间一个元素少了一个像素?如何对齐subpixel和pixel之间的转换有两种方式,一种是enclosingIntRect,一种是pixelSnappedIntRect。上面的例子中使用了第二种转换方法。上图中,灰色网格代表物理像素,蓝色区域代表子像素布局的计算值,黑色区域代表子像素->像素的最终对齐结果。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的计算也很简单,直接四舍五入到最近的物理像素。按照上面的例子,我们现在将50px分层分成6个部分进行模拟计算,看看每个部分的计算宽度应该是多少:1.containerwidth:50px*64=>32002.eachsub-div:3200/6=round(533.333333333)=>5333。最终计算值:533/64=>8.328125//log0宽度:8,计算宽度:8.3281251宽度:9,计算宽度:8.3281252宽度:8,计算宽度1:258,宽度28计算宽度:8.3281254width:9,computedwidth:8.3281255width:8,computedwidth:8.328125看到js计算出来的值和我们计算出来的一致,不是简单的50/6=8.333333333。最后渲染时:第一个元素:直接从容器左侧绘制,发现多余的8.328125个小数解不出来直接四舍五入到最近的物理像素得到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=>8像素。第五元素:33+8.328125+0.3125=>41.640625=>round(41.640625)=>42;42-33=>9像素。第六元素:remaining50-41.640625=>8.359375,对齐最近的8px剩余空间。和上面js得到的clientWidth结果8,9,8,8,9,8完全一样。所以这里的元素大小可以通过pixelSnappedIntRect对齐来解释为什么有些元素会多/少一个像素,出现“无规则”。上面介绍的两种对齐方式如何选择,那么WebKit在什么场景下使用什么算法呢?所有布局都会使用亚像素布局吗?为了保证在某些场景下渲染的一致性,并不是所有的场景都会使用subpixel。例如,计算边界时不会使用它。这使我们无法设置边框,并且渲染的元素可能在顶部比在底部更多。1px。而pixelSnappedIntRect在大多数场景计算元素大小时都会用到。enclosingIntRect计算用于少数情况,例如RenderBlock中的SVG框,因为它需要确保框可以完全包含子树。具体细节可以在WebKit的文档或源代码中找到。最佳实践?rem布局中经常出现同一组件不同结果的布局计算值小数导致的渲染结果不一致。由于DPR的转换,部分设备下很多场景都是小数点。例如下面是一个常见的真实业务场景。在实现一个popup组件或者dialog组件的时候,往往会有一些选项,css也可以绘制,比如上面的九个红点,都是一个组件,期望的宽高都是10px,但是通过一系列的conversions之后,第一个变成9px,第二个变成10px。九个起源呈现不同。下面实现的选项图标没有问题。为了避免这种不一致,我们选择使用图片、svg或者直接base64一个png放到组件中,但是如果我们把png图片作为背景放到一个固定宽高的盒子里,还是有可能会出现问题。背景分割如果有一个宽高为10px的容器,设置了一个和容器一样大小的backgroundImage,背景可能会被“分割”,因为容器可能被渲染为9×10,部分内容为背景图像将不可见。这个问题可以通过给容器设置一点填充来解决。还有很多情况渲染结果不符合我们的预期,基本上都是使用了rem布局造成的。解决办法就是rem转px的时候尽量不要做小数,或者直接用px,或者不用rem布局!本文demo请参考https://codepen.io/Jiavan/pen...推荐阅读https://trac.webkit.org/wiki/...如有错误欢迎指正,文章原文https://github.com/Jiavan/blo...转载请注明出处。