移动端 1px 的解决方案

657 阅读5分钟

背景

在高清屏(Retina)下移动端的 1px 会很粗, 如下面图一是假的 1 像素,图二是真的 1 像素 image.png

image.png

产生原因

  • 主要是跟 DPR(device Pixel Ratio) 设备像素比有关,它是默认缩放为 100% 的情况下设备像素和 CSS 像素的比值 window.devicePixelRatio = 物理像素 / CSS像素
  • 目前主流的屏幕 DPR=2 (iPhone 8)、3(iPhone 8 Plus),拿 2 倍屏来说,设备的物理像素要实现 1 像素,而 DPR=2,所以 CSS 像素只能是 0.5;一般设计稿是按照 750 来设计的,它上面的 1px 是以 750 来参照的,而写 CSS 样式是以设备 375 为参照的,所以应该写 0.5px 就好了,iOS 8+ 系统支持,安卓系统不支持

解决方案

WWDC 对 iOS 系统给出的方案

0.5px 的边框

border: 0.5px solid #E5E5E5;

问题是 Retina 屏的浏览器可能不认识 0.5px 的边框,将会把它解释成 0px 没有边框,包括 iOS 7 和 之前版本、OS X Mavericks 及以前版本,Android 设备在 3 倍屏下不是 0.3333px ,在 Chrome 上模拟 iPhone 8Plus,发现小于 0.46px 时是显示不出来

总结
1)优点:简单,没有副作用
2)缺点:支持 iOS 8+,不支持安卓,后期安卓 follow 就好了

解决方案
通过 JS 检测浏览器能否处理 0.5px 的边框,若可以则给 元素添加个class,看起来是个解决方案,可是要兼容安卓设备,和 iOS 8 以下设备怎么办?这个思路行不通

// 脚本应该放在<body>内, 若在<head>里面运行,需包装 $(document).ready(function() {})
if (window.devicePixelRatio && devicePixelRatio >= 2) {
  var testElem = document.createElement('div');
  testElem.style.border = '.5px solid transparent';
  document.body.appendChild(testElem);
  if (testElem.offsetHeight == 1)
  {
    document.querySelector('html').classList.add('hairlines');
  }
  document.body.removeChild(testElem);
}
// 然后,极细的边框样式就容易了
div {
  border: 1px solid #bbb;
}
.hairlines div {
  border-width: 0.5px;
}

使用边框图片

图片的颜色就是此后 border 的颜色

border: 1px solid transparent;
border-image: url('./../../image/96.jpg') 2 repeat;

总结
1)优点:没有副作用
2)缺点:border 颜色变了就得重新要改图片;圆角会比较模糊

使用 box-shadow 实现

前面两个值 x,y 主要控制显示哪条边,后面两值控制的是阴影半径、扩展半径

box-shadow: 0  -1px 1px -1px #e5e5e5,   //上边线
            1px  0  1px -1px #e5e5e5,   //右边线
            0  1px  1px -1px #e5e5e5,   //下边线
            -1px 0  1px -1px #e5e5e5;   //左边线

总结
1)优点:使用简单,圆角也可以实现
2)缺点:模拟的实现方法,仔细看可看出来这是阴影不是边框

淘宝M站是通过 viewport + rem 实现的

在 devicePixelRatio = 2 时,输出 viewport

<meta name="viewport" content="initial-scale=0.5, maximum-scale=0.5, minimum-scale=0.5, user-scalable=no">

在 devicePixelRatio = 3 时,输出 viewport

<meta name="viewport" content="initial-scale=0.3333333333333333, maximum-scale=0.3333333333333333, minimum-scale=0.3333333333333333, user-scalable=no">

同时通过设置对应 viewport 的 rem 基准值,这种方式就可以像以前一样轻松愉快的写 1px

总结
1)优点:全机型兼容,直接写 1px 更方便
2)缺点:适用于新的项目,老项目可能改动大

<html>
  <head>
      <title>1px question</title>
      <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
      <meta name="viewport" id="WebViewport" content="initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">        
      <style>
          html {
              font-size: 1px;
          }            
          * {
              padding: 0;
              margin: 0;
          }
          .top_b {
              border-bottom: 1px solid #E5E5E5;
          }

          .a,.b {
              box-sizing: border-box;
              margin-top: 1rem;
              padding: 1rem;                
              font-size: 1.4rem;
          }

          .a {
              width: 100%;
          }

          .b {
              background: #f5f5f5;
              width: 100%;
          }
      </style>
      <script>
          var viewport = document.querySelector("meta[name=viewport]");
          //下面是根据设备像素设置viewport
          if (window.devicePixelRatio == 1) {
              viewport.setAttribute('content', 'width=device-width,initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no');
          }
          if (window.devicePixelRatio == 2) {
              viewport.setAttribute('content', 'width=device-width,initial-scale=0.5, maximum-scale=0.5, minimum-scale=0.5, user-scalable=no');
          }
          if (window.devicePixelRatio == 3) {
              viewport.setAttribute('content', 'width=device-width,initial-scale=0.3333333333333333, maximum-scale=0.3333333333333333, minimum-scale=0.3333333333333333, user-scalable=no');
          }
          var docEl = document.documentElement;
          var fontsize = 32* (docEl.clientWidth / 750) + 'px';
          docEl.style.fontSize = fontsize;
      </script>
  </head>
  <body>
      <div class="top_b a">下面的底边宽度是虚拟1像素的</div>
      <div class="b">上面的边框宽度是虚拟1像素的</div>
  </body>
</html>

image.png

伪类 + transform

原理是把原先元素的 border 去掉,然后利用 :before 或 :after 重做 border,并将 transform 的 scale 缩小一半,原先的元素相对定位,新做的 border 绝对定位

单条 border

.hairlines li {
    position: relative;
    border:none;
}
.hairlines li:after  {
    content: '';
    position: absolute;
    left: 0;
    background: #000;
    width: 100%;
    height: 1px;
    -webkit-transform: scaleY(0.5);
            transform: scaleY(0.5);
    -webkit-transform-origin: 0 0;
            transform-origin: 0 0;
}

// 将伪元素设置绝对定位并且和父元素的左上角对齐,将 width 设置 100%,height 设置为 1px
// 然后进行在Y方向缩小0.5倍
.setOnePx {
  position: relative;
  &::after {
    position: absolute;
    content: '';
    background-color: #e5e5e5;
    display: block;
    width: 100%;
    height: 1px; /*no*/
    transform: scale(1, 0.5);
    top: 0;
    left: 0;
  }
}

四条 border

.hairlines li{
    position: relative;
    margin-bottom: 20px;
    border:none;
}
.hairlines li:after{
    content: '';
    position: absolute;
    top: 0;
   left: 0;
   border: 1px solid #000;
   -webkit-box-sizing: border-box;
   box-sizing: border-box;
   width: 200%;
   height: 200%;
   -webkit-transform: scale(0.5);
   transform: scale(0.5);
   -webkit-transform-origin: left top;
   transform-origin: left top;
}

// 同样为伪元素设置绝对定位并且和父元素左上角对其
// 将伪元素的长和宽先放大 2 倍
// 然后再设置一个边框,以左上角为中心,缩放到原来的 0.5 倍
.setBorderAll {
     position: relative;
     &:after {
         content:" ";
         position:absolute;
         top: 0;
         left: 0;
         width: 200%;
         height: 200%;
         transform: scale(0.5);
         transform-origin: left top;
         box-sizing: border-box;
         border: 1px solid #E5E5E5;
         border-radius: 4px;
    }
}

// 样式使用的时候,也要结合 JS 代码,判断是否 Retina 屏
if(window.devicePixelRatio && devicePixelRatio >= 2) {
    document.querySelector('ul').className = 'hairlines';
}

总结
1)优点:全机型兼容,实现了真正的 1px,而且可以圆角
2)缺点:暂用了 after 伪元素,可能影响清除浮动;<td> 用不了
3)可以参照这个 border 生成器 写一个 scss 的 1px 边框生成器,该生成器考虑了三种情况:单边边框、多边边框、边框的圆角

踩坑

使用伪元素方法,伪类里面再设置伪元素,可以选择到吗? 看图,需要修改中间的竖线 image.png 代码,然而展示出来的样式是下面这样的

&:nth-child(2) {
    // border-color: #e5e5e5 !important;
    border: 0;
    position: relative;
    &:after {
        position: absolute;
        content: '';
        background-color: #e5e5e5;
        display: block;
        width: 100%;
        height: 1px; /*no*/
        transform: scale(1, 0.5);
        top: 0;
    }
}

image.png

为什么中间的竖线没有了?在伪类下面再写伪元素after是可以的,这里的原因是 width 和 height 设置有问题,对于竖线应该是 width = 1px,height = 100%,然后再缩放 X 方向 0.5 倍,这样竖线就出来了;同样设置水平线应该是 width = 100%,height = 1px,然后再缩放 Y 方向 0.5 倍

&:nth-child(2) {
    position: relative;
    &:after {
      position: absolute;
      content: '';
      top: 0;
      left: 0;
      width: 1px;
      height: 100%;
     transform: scaleX(0.5);
     background: #e5e5e5;
     transform-origin: 0 0;
   }
}
// 或
 &:nth-child(2) {
    position: relative;
    &:after {
      content:" ";
      position:absolute;
      top: 0;
      left: 0;
      width: 200%;
      height: 200%;
      transform: scale(0.5);
      transform-origin: left top;
      box-sizing: border-box;
      border-left: 1px solid #E5E5E5;
    }
}