移动端1px像素问题的根本原因 | 优质解决方案

7,286 阅读8分钟

前言


发现这个问题是因为验收过程中被设计小伙伴吐槽在不同手机设备下同样是1px宽度的border却显示不同宽度。

在和设计小伙伴一番沟(zheng)通(bian)之后,我陷入了沉思并且提出了以下问题。

问题

  • 1px的设置是没有问题的,那么是什么导致了不同设备的表现不一致呢?
  • 我和设计小伙伴嘴里说的1px是一个东西吗?
  • 有哪些解决方法?
  • 能不能抽象出一个优质又通用的解决方案?

概念


要解答上述的问题,首先我们需要的就是先理清楚相关的概念。

在CSS规范中,长度单位可以分为两类,绝对(absolute)单位以及相对(relative)单位。px是一个相对单位,相对的是设备像素(device pixel)。

  • PX(CSS pixels)

虚拟像素,可以理解为“直觉”像素,CSS和JS使用的抽象单位,浏览器内的一切长度都是以CSS像素为单位的,CSS像素的单位是px。

px其实是pixels的缩写,它是组成图像的基本单元,但正如前面所说的,它是一个抽象的概念,它并不是一个块,也不是一个确定值,它只是一个会在不同情况下适应性变化的抽象单位。

即便是在同一个设备中,css像素也是会发生变化的,例如随意打开一个网页按住command 和 + 号键你就可以把页面整个放大。这个时候浏览器没变,宽度没有变,变的只是css像素的大小。

  • DP(device pixels)

设备像素(物理像素),顾名思义,显示屏是由一个个物理像素点组成的,通过控制每个像素点的颜色,使屏幕显示出不同的图像,屏幕从工厂出来那天起,它上面的物理像素点就固定不变了,单位pt。

pt在css单位中属于真正的绝对单位,1pt = 1/72(inch),inch及英寸,而1英寸等于2.54厘米。

  • DPR(device pixels ratio)

设备像素比(dpr 描述的是未缩放状态下,物理像素和CSS像素的初始比例关系,计算方法如下图。

获得设备像素比(dpr)后,便可得知设备像素与CSS像素之间的比例。

DPR = 设备像素/CSS像素

  • DIP(Device independent Pixel)

设备独立像素/逻辑像素

一般的理解来说,可以理解为: 设备独立像素 = css像素

所以同样的 DIP是一个计算得出的值,它真正控制着元素的大小。

下面来举个🌰:

  • 需求: 现在有一幅750px下的设计图。需要展示一个1px的border。实际开发为375px,设计同学想在无论那种屏幕下,最终呈现的都是1pt的效果。

  • DPR=1 即一倍屏表现:

    我们设 1dpi在1倍屏中真实展示x个pt

    根据公式 可以得出 1dpi = 1px = x/1 即 x = 1,

    1dip刚好显示1个pt,符合设计同学的要求

  • DPR=2 即二倍屏表现:

    我们设 1dpi在2倍屏中真实展示x个pt

    根据公式 可以得出 1dpi = 1px = x/2 即 x = 2,

    1dip 显示 2个pt。出大问题😂

  • 三倍屏同理

    1dip = 3pt

根本原因


看完上面的计算,大家想必明白了引起这个问题的原因。正是因为不同设备可能有不同的dpr,导致了1px展示的宽度有所不同。

那我们可以回头回答一下之前提出的问题

  • 问:1px的设置是没有问题的,那么是什么导致了不同设备的表现不一致呢?

    答: 不同的dpr导致了不同设备的1px包含了不同数量的pt

  • 问:我和设计小伙伴嘴里说的1px是一个东西吗

    答:并不是,设计小伙伴真正想要的是1pt的效果

解决方案


了解了根本原因那么剩下的就是解决他了。

有的小伙伴说那我在两倍屏把宽度定义成0.5px不就行了吗。但先不说分割1px基础单位略微有点奇怪,而且0.5px也只在ios8+上支持,安卓上是不行的。。有兴趣的同学可以搜索 2014年的 WWDC 上提出的这个议案。实现也是比较简单的。

所以古往今来避免1px问题的方法有很多,不外乎两类:

  • 其他属性模拟0.5px效果
  • 跳过设置1px这个话题

background-image && boder-image


典型的跳过设置1px这个问题的解决方法

    .background-image-1px {
      background: url(...) no-repeat left bottom;
      -webkit-background-size: 100% 1px;
      background-size: 100% 1px;
    }
    
    .border-bottom-1px {
      border-width: 0 0 1px 0;
      -webkit-border-image: url(...) 0 0 2 0 stretch;
      border-image: url(...) 0 0 2 0 stretch;
    }

优势: 至少解决了问题

劣势: 我想换个颜色还得换图,而且图片处理圆角会出现模糊的问题

border-shadow


通过css对border-shadow的处理来模拟0.5px

.box-shadow-1px {
  box-shadow: inset 0px -1px 1px -1px #c8c7cc;
}

优势: 代码少,还能换颜色

劣势: 阴影导致的颜色变浅,而且仔细看谁都看得出这是阴影而不是边框。。。

伪元素 + scale缩放


话说前头,大力推荐。直接上代码。ant-design-mobile同样使用这种处理

@border-color-base : #EBEDF0;
// 伪元素的位置控制
.scale-hairline-common(@color, @top, @right, @bottom, @left) {
  content: '';
  position: absolute;
  background-color: @color;
  display: block;
  z-index: 1;
  top: @top;
  right: @right;
  bottom: @bottom;
  left: @left;
}

//上边框
.hairline(@direction, @color: @border-color-base) when (@direction = 'top') {
  border-top: 1PX solid @color;

    @media (min-resolution: 2dppx) {
      border-top: none;

      &::before {
        .scale-hairline-common(@color, 0, auto, auto, 0);
        width: 100%;
        height: 1PX;
        transform-origin: 50% 50%;
        transform: scaleY(0.5);

        @media (min-resolution: 3dppx) {
          transform: scaleY(0.33);
        }
      }
    }
}

// 右边框
.hairline(@direction, @color: @border-color-base) when (@direction = 'right') {
  border-right: 1PX solid @color;

    @media (min-resolution: 2dppx) {
      border-right: none;

      &::after {
        .scale-hairline-common(@color, 0, 0, auto, auto);
        width: 1PX;
        height: 100%;
        background: @color;
        transform-origin: 100% 50%;
        transform: scaleX(0.5);

        @media (min-resolution: 3dppx) {
          transform: scaleX(0.33);
        }
      }
    }
}

// 下边框
.hairline(@direction, @color: @border-color-base) when (@direction = 'bottom') {
  border-bottom: 1PX solid @color;

    @media (min-resolution: 2dppx) {
      border-bottom: none;
      &::after {
        .scale-hairline-common(@color, auto, auto, 0, 0);
        width: 100%;
        height: 1PX;
        transform-origin: 50% 100%;
        transform: scaleY(0.5);
        @media (min-resolution: 3dppx) {
          transform: scaleY(0.33);
        }
      }
    }
}

// 左边框
.hairline(@direction, @color: @border-color-base) when (@direction = 'left') {
  border-left: 1PX solid @color;

    @media (min-resolution: 2dppx) {
      border-left: none;

      &::before {
        .scale-hairline-common(@color, 0, auto, auto, 0);
        width: 1PX;
        height: 100%;
        transform-origin: 100% 50%;
        transform: scaleX(0.5);

        @media (min-resolution: 3dppx) {
          transform: scaleX(0.33);
        }
      }
    }
}

// 全边框
.hairline(@direction, @color: @border-color-base, @radius: 0) when (@direction = 'all') {
  border: 1PX solid @color;
  border-radius: @radius;

    @media (min-resolution: 2dppx) {
      position: relative;
      border: none;

      &::before {
        content: '';
        position: absolute;
        left: 0;
        top: 0;
        width: 200%;
        height: 200%;
        border: 1PX solid @color;
        border-radius: @radius * 2;
        transform-origin: 0 0;
        transform: scale(0.5);
        box-sizing: border-box;
        pointer-events: none;
      }
    }
}

具体使用:

.div1{
    .hairline('top',#eeeeee)
    .hairline('right')
    .hairline('left',#cccccc)
}
.div2{
    .hairline('all',#bbbbbb,3px)
}

优势:使用less将css接口化,功能灵活,复用性和兼容性较好,通过媒体查询适配了2倍屏和3倍屏。该代码写在公共css样式中即可。

劣势:代码仍然有继续抽象的空间,并且此方案是使用positon:absolute进行定位,所以父元素也必须满足绝对定位的触发条件。

结尾

综上,开头的4个问题全部回答完毕。这个问题也得到了解决。

虽然看似是个简单的问题,但是延伸的知识点却很多。总结一下个人的收获

  • 屏幕显示相关概念(px,dp,dpr,dip)与他们互相之间关系的了解
  • 复习了border-image和background-image以及border-shadow属性
  • 更深入的了解了css接口化的设计思想。
  • 翻阅了更多less的深入使用方式
  • 获得了移动端1px比较优质的解决方案,实际解决了项目问题。

补充

评论里的一个问题问的非常好。既然在不同dpr上会有不同的物理像素数量,为什么在大于1px时没有这个问题呢?

下面转自知乎的一个解释,可谓是非常清楚了。

简单来说就是手机屏幕分辨率越来越高了,同样大小的一个手机,它的实际物理像素数更多了。因为不同的移动设备有不同的像素密度,所以我们所写的1px在不同的移动设备上等于这个移动设备的1px。现在做移动端开发时一般都要加上一句话:

<meta name="viewport" content="width=device-width, 
initial-scale=1.0, maximum-scale=1.0, user-scalable=no">

这句话定义了本页面的viewport的宽度为设备宽度,初始缩放值和最大缩放值都为1,并禁止了用户缩放。viewport的设置和屏幕物理分辨率是按比例而不是相同的,移动端window对象有个devicePixelRatio属性,它表示设备物理像素和css像素的比例,在retina屏的iphone手机上,这个值为2或3, css里写的1px长度映射到物理像素上就有2px或3px。通过设置viewport,可以改变css中的1px用多少物理像素来渲染,设置了不同的viewport,当然1px的线条看起来粗细不一致。