Retina 屏幕移动设备 1px 实线边框的实现

很多人初做 H5 页面时都会有这样的经历,用 CSS 定义 1px 的实线边框,在 @2x 的屏幕上会显示成 2px,在 @3x 的屏幕上会显示成 3px,明明希望是极细的线条,到 Retina 屏上却变得粗大丑陋。这是因为 CSS 中的 px 单位定义的是逻辑像素值,而实际显示的效果会以物理像素呈现,Retina 屏幕的物理像素值和逻辑像素值不同就造成了这种差异。

如果想让 HTML5 页面像 Native App 一样在 Retina 屏幕上显示 1 物理像素的实线,该如何实现呢?
接下来介绍在 @2x 屏幕上的若干实现方案,@3x 同理可得。

一、border-width: 0.5px

2014年的 WWDC 大会上,Ted O’Connor 提到了 iOS 8 和 OS X Yosemite 支持 0.5px,以此实现 “Retina Hairline Border”。

0.5px Retina Hairline Border 的写法

So easy! 不是么?

想法很直接,但旧版 iOS、OS X 和 Android 等无法识别 0.5px 的设备上,多数会解释成 0px,也就是没有边框,这样问题就严重了。

解决方案:

通过 JavaScript 检测浏览器是否支持 0.5px,如果支持,给 <html> 节点添加个 className。

1
2
3
4
5
6
7
8
9
10
if (window.devicePixelRatio && devicePixelRatio > 1) {
    var testEle = document.createElement('div');
    testEle.style.border = '.5px solid transparent';
    document.body.appendChild(testEle);
    if (testEle.offsetHeight == 1) {
        document.querySelector('html').classList.add('hairlines');
    }
    document.body.removeChild(testEle);
}
// This assumes this script runs in <body>, if it runs in <head> wrap it in $(document).ready

然后 Hairline 的 CSS 就很容易了:

1
2
3
4
5
6
.item {
    border: 1px solid #ccc;
}
.hairlines .item {
    border-width: 0.5px;
}

Demo:http://mutian.wang/demo/2015/hairlines/#line1

除了上面的方法,还可以使用 JavaScript 或服务端判断 UA,如果是 iOS 8+,则在 <html> 节点添加 className,从页面整体性能来讲,我更支持从服务端判断 UA,具体代码不再赘述。

缺点:

  1. 需要额外的 JavaScript
  2. 会导致页面重绘
  3. 不支持 0.5px 的浏览器上只能以逻辑分辨率呈现
  4. 目前浮点数的像素值只支持 0.5,还不支持诸如 0.3 等其他小数,在 @3x 的设备上只能使用 0.5px

目前多数桌面版现代浏览器、iOS 和 OS X 的 Safari 8 都已支持 0.5px,安卓版 Chrome 虽然尚未支持,但支持也应该是迟早的事。即使某个浏览器不支持,Hairline 也会显示成一个常规的边框,不会有太大的影响。

二、box-shadow

原理:利用 box-shadow 向内收缩后的残余阴影

1
2
3
4
5
6
7
8
9
.item {
    /* 模拟此效果:
    border: 1px solid rgba(0, 0, 0, .15);
    */
    box-shadow: 0 -1px 1px -1px rgba(0, 0, 0, .5),
                -1px 0 1px -1px rgba(0, 0, 0, .5),
                1px 0 1px -1px rgba(0, 0, 0, .5),
                0 1px 1px -1px rgba(0, 0, 0, .5);
}

Demo:http://mutian.wang/demo/2015/hairlines/#line2

缺点:

  1. 线条边缘会有少量残余的阴影
  2. box-shadow 的颜色值比较难取

三、border-image

1
2
3
4
.item {
    border-width: 1px;
    border-image: url(border.png) 2 repeat;
}

border.png 做成类似下面的 6*6px 图片:

border-image 实现 Retina Hairline Border

Demo:http://mutian.wang/demo/2015/hairlines/#line3

缺点:

  1. 需要额外的图片( 6*6px 的图片只有 51 bytes,为减少请求数可以写成 Data URI 形式)
  2. 不支持圆角
  3. 修改边框的颜色不方便

四、背景渐变

原理:在 Retina 屏幕上,去掉容器的原有 border,设置 1px 宽或高的渐变背景,50% 有颜色,50% 透明

1
2
3
4
5
6
7
8
9
10
11
12
13
.item {
    border: 1px solid #ccc;
}
@media (-webkit-min-device-pixel-ratio: 2), (min-device-pixel-ratio: 2) {
    .item {
        border: none;
        background:
            linear-gradient(180deg, #ccc, #ccc 50%, transparent 50%) top    left  / 100% 1px no-repeat,
            linear-gradient(90deg,  #ccc, #ccc 50%, transparent 50%) top    right / 1px 100% no-repeat,
            linear-gradient(0,      #ccc, #ccc 50%, transparent 50%) bottom right / 100% 1px no-repeat,
            linear-gradient(-90deg, #ccc, #ccc 50%, transparent 50%) bottom left  / 1px 100% no-repeat;
    }
}

Demo:http://mutian.wang/demo/2015/hairlines/#line4

缺点:

  1. 将容器的 border 去掉可能会影响盒模型尺寸
  2. 不支持圆角
  3. 代码复杂

五、动态 viewport

原理:在 devicePixelRatio = 2 时,输出 viewport:

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

在 devicePixelRatio = 3 时,输出 viewport:

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

这样就可以像以前一样轻松愉快地写 border-width: 1px; 了。

Demo:淘宝M首页

缺点:

  1. 部分 App 的 WebView 会忽略 HTML 中定义的 viewport meta 信息(比如新浪微博的安卓客户端)

六、伪对象 + transform 缩放

原理:在 Retina 屏幕上,去掉容器的原有 border,利用 ::before 或 ::after 制造容器尺寸 2 倍或 3 倍的绝对定位伪对象,使用 1px 的 border 定义新边框后,用 transform 的 scale 把伪对象缩小到一半或 1/3,这样看上去伪对象就和容器一样大了

单边框:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.item {
    border-bottom: 1px solid #ccc;
}
@media (-webkit-min-device-pixel-ratio: 2), (min-device-pixel-ratio: 2) {
    .item {
        position: relative;
        border-bottom: none;
    }
    .item::after {
        content: ' ';
        display: block;
        position: absolute;
        left: 0;
        bottom: 0;
        width: 200%;
        height: 0;
        border-bottom: 1px solid #ccc;
        -webkit-transform: scale(0.5) translate(-50%, -50%);
        transform: scale(0.5) translate(-50%, -50%);
    }
}

四周边框:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.item {
    border: 1px solid #ccc;
}
@media (-webkit-min-device-pixel-ratio: 2), (min-device-pixel-ratio: 2) {
    .item {
        position: relative;
        border: none;
    }
    .item::after {
        content: ' ';
        display: block;
        position: absolute;
        top: 0;
        left: 0;
        -webkit-box-sizing: border-box;
        box-sizing: border-box;
        width: 200%;
        height: 200%;
        border: 1px solid #ccc;
        -webkit-transform: scale(0.5) translate(-50%, -50%);
        transform: scale(0.5) translate(-50%, -50%);
    }
}

Demo:http://mutian.wang/demo/2015/hairlines/#line6

缺点:

  1. 占用 ::before 或 ::after 伪对象
  2. 将容器的 border 去掉可能会影响盒模型尺寸
  3. 容器会变成定位容器
  4. 代码复杂

总结:

以上方法都可以实现 Hairline,但每种方法都有明显的缺点,实际项目中哪种更优,还需要针对这些缺点进行权衡。

~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~

参考资料: