Retina屏幕中0.5px边框效果的CSS实现



引言

刚接触 iOS 7 时,并没有太过在意系统中随处可见的边线,后来的一个项目中,“设计师”始终觉得前端做出来的边框和视觉稿不一样,其想要的效果是像系统中那样细的边线:

iOS Setting

经过一顿谷歌,大概搞懂了 iOS 7 中这种特别细的边线的显示原理,以及如何使用 CSS 实现同样的效果。

原理

首先想用一张图来解释像素、渲染像素、物理像素之间的关系:

像素、渲染像素、物理像素

在 iPhone 4 以前,「像素」和「物理像素」是一一对应的,设计中的 1 个点对应屏幕硬件上的 1 个像素点。而 iPhone 4 之后,Retina 屏幕出现,在 Retina 屏幕上,使用 4 个硬件上的像素点 (2 x 2) 来表示 1 个设计稿上的像素点。而「渲染像素」,在 iPhone 6 之前的设备无需在意,因为 Display Zoom 这个功能是从 iPhone 6 (Plus) 才开始加入,Downscale 的问题也只会出现在 iPhone 6 Plus 中。

「像素」分辨率是设计时应当参考的尺度;
「渲染像素」分辨率是系统基于「像素」分辨率进行倍增(1x、2x 或 3x)得到的结果;
「物理像素」分辨率是指设备屏幕本身的分辨率;

拿 iPhone 5s 来举例,其「物理像素」分辨率为 640x1136,但是它的浏览器是按照 320x568 进行渲染的,也就是说会用 4 个「物理像素」表示 1 个 CSS「像素」,所以网页中一个 2px*2px 的元素实际上是由 16 个「物理像素」组成的:

Retina Explanation

网页中设置边框宽度为 1px 时,iPhone 5s 下的效果:

1px border demo

可以看出:虽然给该边框设置的宽度是 1px,但是它在屏幕上却占据着 2 个物理像素点。
然而设计师想要的效果是这样的:

0.5px border demo

上图的边框明显比 “1px border demo” 要细,也更加像系统本身的边框,因为它在屏幕上只占据了 1 个物理像素点。

我们把这种用 1 个物理像素表示的细线称为 hairline,那么如何使用 CSS 实现 hairline 呢?

实现

下面就介绍几种 hairline 的实现方式:

1. background-image(image) + background-size [demo]

优势:原理简单;各机型下的效果都比较好;
缺点:不支持圆角;改变边框颜色需要重新做图,不方便;

1
2
3
4
5
6
7
8
9
.list li {
position: relative;
font-size: 16px;
font-weight: bold;
padding: 15px;
box-sizing: border-box;
background: #f8f8f8 url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACAQMAAACjTyRkAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAGUExURUxpceHh4aIUC8kAAAABdFJOUwBA5thmAAAADElEQVQI12NgYGgAAACEAIEYVvHNAAAAAElFTkSuQmCC") left bottom repeat-x;
background-size: auto 1px;
}

2. border + scale [demo]

优势:可自由控制边框颜色;支持圆角;
缺点:改变太多元素属性;大量使用时 Android 下可能会产生性能问题;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
.list li {
position: relative;
display: inline-block;
width: 140px;
padding: 20px 0;
font-size: 16px;
font-weight: bold;
margin: 0 10px 10px 0;
border-radius: 5px;
box-sizing: border-box;
background: #f8f8f8;
}
.list li::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 200%;
height: 200%;
border: 1px solid #aaa;
border-radius: 10px;
box-sizing: border-box;
transform: scale(0.5);
transform-origin: left top;
pointer-events: none;
}

3. background-image(linear-gradient) + background-size [demo]

优势:可自由控制边框颜色;
缺点:改变太多元素属性;不支持圆角;Android 对 gradient 的支持不够友好,边框效果不理想;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
.list li {
position: relative;
display: inline-block;
width: 140px;
padding: 20px 0;
font-size: 16px;
font-weight: bold;
text-align: center;
margin: 0 10px 10px 0;
box-sizing: border-box;
background: #f8f8f8;
}
.list li::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: -webkit-linear-gradient(top, #aaa, #aaa 60%, transparent 60%),
-webkit-linear-gradient(right, #aaa, #aaa 60%, transparent 60%),
-webkit-linear-gradient(bottom, #aaa, #aaa 60%, transparent 60%),
-webkit-linear-gradient(left, #aaa, #aaa 60%, transparent 60%);
background-size: 100% 1px, 1px 100%;
background-position: top, right top, bottom, left top;
background-repeat: no-repeat;
pointer-events: none;
}

4. 动态改变 viewport [demo]

优势:基本上支持一切使用场景;
缺点:

  • 需要一段脚本动态计算并改写 viewport 设置;
  • 页面布局必须采用 rem,以等比缩放的方式进行适配(不符合行业标准适配规则);
  • 由于需要动态改变 viewport,但是部分 Android 下 viewport 的各个属性生效时机不一致,导致页面渲染过程中出现明显的修正过程,暂未找到较理想的解决方案;

核心脚本:

1
2
3
4
5
6
7
8
9
var docEl = document.documentElement;
var metaEl = document.querySelector('meta[name="viewport"]');
var dpr = win.devicePixelRatio || 1;
scale = 1 / dpr;
// 中间经过各种异常 case 处理 ...
var docWidth = docEl.clientWidth;
metaEl.setAttribute('content', 'width=' + dpr * docWidth + ',initial-scale=' + scale + ',maximum-scale=' + scale + ', minimum-scale=' + scale + ',user-scalable=no');

Demo 样式:

1
2
3
4
5
6
7
8
.list li {
position: relative;
font-size: calc(32rem/75);
font-weight: bold;
padding: calc(30rem/75);
box-sizing: border-box;
border-bottom: 1px solid #aaa;
}

以上几种方案虽然实现方式上不同,但基本上都是通过缩放来实现的,不管是背景缩放、边框缩放,还是整个页面缩放。
这些方案都有自己无法解决的问题,并不理想,我们来看下更理想的方案吧。

更理想的方案

2014 年的 WWDC 大会上,Ted O’Connor 在 “Designing Responsive Web Experiences” 中提出 “retina hairlines” 的概念,并提到了开发者该如何实现。

iOS 8 以及 OS X Yosemite 开始支持 0.5px,看起来就是为了解决这个问题而加入的。那我们是不是就可以直接给元素的边框宽度设置为 0.5px 了呢?并没有那么简单,因为不是所有的浏览器都支持 0.5px,所以我们必须要写兼容逻辑,具体实现方案如下:

[demo]

检测浏览器是否支持 0.5px,如果支持则在 HTML 元素上增加 hairlines 的 class:

1
2
3
4
5
6
7
8
9
10
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);
}
// This assumes this script runs in <body>, if it runs in <head> wrap it in $(document).ready(function() {})

为元素单独设置 0.5px 的样式:

1
2
3
4
5
6
7
div {
border: 1px solid #bbb;
}
.hairlines div {
border-width: 0.5px;
}

应该会有人问那 Android 以及 iOS 8 以下的机器怎么办?关于这点我是这样认为的:

  • retina 屏幕越来越普及,针对 retina 屏幕的特性浏览器都会慢慢都支持的;
  • retina hairlines 本来就属于增强体验,我们应该以渐进增强的思维来处理这种 case;
  • Android 下为了实现 retina hairlines 而增加那么多冗余代码,而且可能会产生一些不可预知的问题,实在有些得不偿失;

注释

  1. 前提是将浏览器的 viewport 设置为:<meta name="viewport" content="width=device-width,initial-scale=1">

参考引用

像素、渲染像素以及物理像素是什么东西?它们有什么联系?
Half-Point CSS Borders in iOS
CSS retina hairline, the easy way.