Skip to content

探究 1px 问题

发布日期:2024-02-04

1px 问题是前端开发中常见问题,会使得你的边框在某些设备上无法显示或者过粗过细,不想测试和设计找你麻烦的话,就要好好研究了o( ̄︶ ̄)o


1px 问题的成因

1px 问题是指在Retina显示屏(由苹果定义的,高像素密度的屏幕,可以使肉眼看不见单个物理像素点)上 1px 会变得很粗,显示出不止 1px 的大小。

这是因为在高像素密度的屏幕上,一个逻辑像素会以多个物理像素来渲染。比如对于 iphone6,其逻辑分辨率为 375x667pt,物理像素为 750x1334px,以两个物理像素渲染一个逻辑像素。

题外话:什么是pt?

对于 iphone 和 ios 开发来说,pt 是独立像素的意思,不随屏幕像素密度变化而变化,也就是逻辑像素。对于安卓来说是 dp(与密度无关的像素)。因此描述 iphone6 分辨率的时候,可以表示为 375x667pt。但通常我们搜索 pt 这个单位的含义的时候,通常会找到另一种答案:pt是印刷行业常用单位,等于1/72英寸。

于是,当设计师们在 750 宽度的设计稿(二倍设计稿)上,标示出 1px 的边框时,实际转换到逻辑像素是 0.5 px,因此如果前端还是写 1px,线条就会比设计稿来的粗。

直接写0.5px就可以了吗?

0.3px边框的盒子
0.5px边框的盒子
1px 边框的盒子

部分安卓浏览器、ios8 不支持 0.5px 写法,而且不同浏览器对小数点的四舍五入各不相同,0.5px 在不同设备可能会显示出不同粗细的线条,因此任何带有小数点的像素都可能出现类似的问题。而 pc 端也可能会把 0.5 px 渲染为 1px。

可以对 0.5px 兼容性进行判断,再实施不同兼容方案,测试 0.5px 兼容性的方法如下

if (dpr >= 2) {
  var docEl = document.documentElement
  var fakeBody = document.createElement('body')
  var testElement = document.createElement('div')
  testElement.style.border = '.5px solid transparent'
  fakeBody.appendChild(testElement)
  docEl.appendChild(fakeBody)
  if (testElement.offsetHeight === 1) {
    docEl.classList.add('hairlines')
  }
  docEl.removeChild(fakeBody)
}

伪元素缩放

使用了伪元素缩放的盒子,实现0.5px

通过为元素设置伪元素,设置 1px 的边框,再缩放为原尺寸的二分之一,实现了0.5px 边框,css 代码如下

.border {
  position: relative;
}
.border::after {
  content: "";
  box-sizing: border-box;
  position: absolute;
  left: 0;
  top: 0;
  width: 200%; 
  height: 200%; 
  border: 1px solid gray;
  transform: scale(0.5); 
  transform-origin: 0 0;
  border-radius: 16px;
}

基于 flexible 的动态设置 viewpoint

虽然 flexible 自适应方案目前已经停止维护,官方也表示该方案已过时,可以寻找其他现代化的适配方案。但这个方案在那时候(2014年5月)是非常出色的,因为2014年发布的 iphone6 为移动端适配带来了新的问题(多倍屏),flexble 方案的出现为前端开发者带来了全新的解决方案。

flexible 方案通过动态设置 viewpoint 的 meta 标签,将 initial-scale 值设置为 1/dpr,将页面缩放为原来的 1/dpr,从而使得 css 像素 = 物理像素。之后修改 html 元素的 font-size,使用 rem 布局将页面平分为10份,每份是1rem,假设缩放后页面的宽度是 750px,则 1rem = 75px,因此 font-size = 75px。当我们拿到 750px 设计稿时,我们需要画一个 100px 的元素,此时只需计算 100px/75 = 1.33rem 就可以得到 css 单位。于是无论设备屏幕像素怎样变化,1.33rem 换算到屏幕上总能符合设计稿的尺寸,还原设计稿的时候就只需要计算 css像素rem = 元素像素/(设计稿尺寸*10),这个过程也可以由编辑器插件或者 postcss 的 px2rem 完成。

回到 1px 问题上,由于进行了页面缩放使得 css 像素 = 物理像素,因此我们照常将 1px 进行上述换算,rem 转换回 px 之后也会得到 1px(但不排除在转化过程中会产生小数点),避免了使用 0.5px 导致的兼容性问题。但由于安卓设备兼容性不佳,这个解决方案在安卓设备上固定不进行缩放,因此使用此方案在安卓上无法完美解决 1px 问题。

使用Flexible实现手淘H5页面的终端适配

淘宝的flexible适配方案为什么只对iOS进行dpr判断,对于Android始终认为其dpr为1?

box-shadow

box-shadow: 0 -1px #D9D9D9, 1px 0 #D9D9D9, 0 1px #D9D9D9, -1px 0 #D9D9D9;border-radius: 16px;

缺点是占用 box-shadow 属性。

background

背景图片渐变

缺点是无法实现圆角,在 pc 上显示异常。

.background-border {
   background-image: linear-gradient(180deg, red, red 50%, transparent 50%), linear-gradient(
      270deg,
      red, 
      red 50%,
      transparent 50%
    ), linear-gradient(0deg, red, red 50%, transparent 50%), linear-gradient(90deg, red, red 50%, transparent
        50%);
  background-size: 100% 1px, 1px 100%, 100% 1px, 1px 100%;
  background-repeat: no-repeat;
  background-position: top, right top, bottom, left top;
  padding: 10px;
}

查看设备 dpr

查询设备dpr通常使用window.devicePixelRatio,这个属性在 caniuse 上的兼容性达到 98%,基本上不用担心无法支持的情况。

根据window.devicePixelRatio,当前设备的 dpr 是:

对于css可以使用 @media (-webkit-min-device-pixel-ratio: 1.5),(min-device-pixel-ratio: 1.5) 来进行媒体查询。

总结

background 渐变、box-shadow 模拟等,都存在缺点,比如限制了正常的 box-shadow 和 background 的使用;直接使用 0.5 px 会存在兼容性问题;viewpoint 和 伪元素缩放两种方式是兼容性比较好的解决方案。

参考

iOS 中 pt 与px的区分

前端移动端1px问题及解决方案