文章目录
  1. 1. 前言
  2. 2. 背景
  3. 3. 一些概念
  4. 4. 几个问题
    1. 4.1. retina下,图片高清问题
    2. 4.2. retina下,border: 1px问题
    3. 4.3. 多屏适配布局问题
    4. 4.4. 字体大小问题
  5. 5. 总结

前言

  对于移动端重构,又是一大令人头疼的问题,每次开始一个项目前,我都要认真思考一番使用何种布局方式会比较好,使用什么库比较好,使用的库会不会产生不兼容等种种问题。为了以后的我做项目前可以少一点纠结,我决定总结一下关于移动端如何进行适配的问题。
  相关代码示例已经上传到Github。   

背景

  1. 开发移动端H5页面
  2. 面对不同分辨率的手机
  3. 面对不同屏幕尺寸的手机

一些概念

  • 三个需要了解的概念:

    • PPI: 可以理解为屏幕的显示密度
    • DPR: 设备物理像素和逻辑像素的对应关系,即物理像素/逻辑像素
    • Resolution: 就是我们常说的分辨率
  • 物理像素与逻辑像素
    mobile
    看了我们上面内容一的第一点之后,或许有些人会有些疑问,我的安卓手机,或者iphone6plus(目前应该仅限于这一款机型吧),买回来的是1920x1080的或者其他更高的,比我之前所谓的那个viewport默认的980px要大。

    这样的问题,也就是我之前所说的物理像素与逻辑像素的关系了(即DPR)。以1920x1080为例,1080为物理像素,而我们在viewport中,获取到的,比如”width-device”,是逻辑像素。所以之前viewport的默认值,所比对的大小,其实是逻辑像素的大小,而非物理像素的大小。

    以iphone6为例,在不做任何缩放的条件下,iphone6的获取到的’width-device’为375px,为屏幕的逻辑像素。而购买时我们所知的750px,则为屏幕的物理像素。

  • 分辨率和像素
    经新xcode6模拟器验证(分辨率为pt,像素为真实pixel):

  1. iPhone5分辨率320×568,像素640×1136,@2x
  2. iPhone6分辨率375×667,像素750×1334,@2x
  3. iPhone6 Plus分辨率414×736,像素1242×2208,@3x,(注意,在这个分辨率下渲染后,图像等比降低pixel分辨率至1080p(1080×1920))
Resolution

  • 设备像素比DPR(device pixel ratio)
    在javascript中,可以通过window.devicePixelRatio获取到当前设备的dpr。

    在css中,可以通过-webkit-device-pixel-ratio-webkit-min-device-pixel-ratio-webkit-max-device-pixel-ratio进行媒体查询,对不同dpr的设备,做一些样式适配(这里只针对webkit内核的浏览器和webview)。

  • CSS的问题
    有了上面第二点的一些基础,还是以iphone6为例,我们可以知道,其实我们所写的1px,在iphone6上为2px的物理像素。所以,最后的,给出一个结论。就是我们写的1px,在移动端,是逻辑像素的1px,而非物理像素的1px。

    所以说,在iPhone6下,如果设计稿为750×1334,而设备宽高为375×667,由于它的DPR为2,所以说切下来的图不会失真;但是如果把这张图放到非retina 屏幕(即普通屏幕DPR为1)下,
    图片就相当于被压缩了,就出现一个物理像素点对应4个位图像素点,所以它的取色也只能通过一定的算法(显示结果就是一张只有原图像素总数四分之一,我们称这个过程叫做downsampling),肉眼看上去虽然图片不会模糊,但是会觉得图片缺少一些锐利度,或者是有点色差(但还是可以接受的)。

    反过来,如果在普通屏幕下切的图设计稿为375×667,放到retina手机上显示,图片就相当于拉伸了,即一个位图像素点要显示4个物理像素点,图片就会模糊。

    所以说,在retina屏幕下,设计稿的位图像素点个数可以跟物理像素点个数形成 1 : 1的比例,图片自然就清晰了(这也解释了为啥dpr为2的视觉稿的画布大小要×2)。

几个问题

retina下,图片高清问题

解决方案:两倍图片(@2x),然后图片容器缩小50%。

如:图片大小,400×600;

1.img标签

1
2
width: 200px;
height: 300px;

2.背景图片

1
2
3
4
width: 200px;
height: 300px;
background-image: url(image@2x.jpg);
background-size: 200px 300px; // 或者: background-size: contain;

这样的缺点,很明显,普通屏幕下:

  1. 同样下载了@2x的图片,造成资源浪费。
  2. 图片由于downsampling,会失去了一些锐利度(或是色差)。
    所以最好的解决办法是:不同的dpr下,加载不同的尺寸的图片。

不管是通过css媒体查询,还是通过javascript条件判断都是可以的。

retina下,border: 1px问题

设计师想要的retina下border: 1px;,其实就是1物理像素宽,对于css而言,可以认为是border: 0.5px;,这是retina下(dpr=2)下能显示的最小单位。
然而,无奈并不是所有手机浏览器都能识别border: 0.5px;,ios7以下,android等其他系统里,0.5px会被当成为0px处理,那么如何实现这0.5px呢?

最简单的一个做法就是这样(元素scale):

1
2
3
4
5
6
7
8
9
10
11
12
13
.scale{
position: relative;
}
.scale:after{
content:"";
position: absolute;
bottom:0px;
left:0px;
right:0px;
border-bottom:1px solid #ddd;
-webkit-transform:scaleY(.5);
-webkit-transform-origin:0 0;
}

我们照常写border-bottom: 1px solid #ddd;,然后通过transform: scaleY(.5)缩小0.5倍来达到0.5px的效果,但是这样hack实在是不够通用(如:圆角等),写起来也麻烦。

多屏适配布局问题

移动端布局,为了适配各种大屏手机,目前最好用的方案莫过于使用相对单位rem

基于rem的原理,我们要做的就是: 针对不同手机屏幕尺寸dpr动态的改变根节点html的font-size大小(基准值)。

这里我们提取了一个公式(rem表示基准值)

1
rem = document.documentElement.clientWidth * dpr / 10

说明:

  1. 乘以dpr,是因为页面有可能为了实现1px border页面会缩放(scale) 1/dpr 倍(如果没有,dpr=1),。
  2. 除以10,是为了取整,方便计算(理论上可以是任何值)
    所以就像下面这样,html的font-size可能会:
1
2
3
4
5
iphone3gs: 320px / 10 = 32px

iphone4/5: 320px * 2 / 10 = 64px

iphone6: 375px * 2 / 10 = 75px

对于动态改变根节点html的font-size,我们可以通过css做,也可以通过javascript做。
css方式,可以通过设备宽度来媒体查询来改变html的font-size:

1
2
3
4
5
6
7
8
9
html{font-size:32;}
//iphone6
@media (min-device-width:375){
html{font-size:75px;}
}
//iphone6 plus
@media (min-device-width:414px){
html{font-size:82.8px;}
}

缺点:通过设备宽度范围区间这样的媒体查询来动态改变rem基准值,其实不够精确,比如:宽度为360px 和 宽度为320px的手机,因为屏宽在同一范围区间内(<375px),所以会被同等对待(rem基准值相同),而事实上他们的屏幕宽度并不相等,它们的布局也应该有所不同。最终,结论就是:这样的做法,没有做到足够的精确,但是够用。

javascript方式,通过上面的公式,计算出基准值rem,然后写入样式,大概如下(代码参考自flexible.js)

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
;(function(win, lib) {
var doc = win.document;
var docEl = doc.documentElement;
var metaEl = doc.querySelector('meta[name="viewport"]');
var flexibleEl = doc.querySelector('meta[name="flexible"]');
var dpr = 0;
var scale = 0;
var tid;
var flexible = lib.flexible || (lib.flexible = {});

if (metaEl) {
console.warn('将根据已有的meta标签来设置缩放比例');
var match = metaEl.getAttribute('content').match(/initial\-scale=([\d\.]+)/);
if (match) {
scale = parseFloat(match[1]);
dpr = parseInt(1 / scale);
}
} else if (flexibleEl) {
var content = flexibleEl.getAttribute('content');
if (content) {
var initialDpr = content.match(/initial\-dpr=([\d\.]+)/);
var maximumDpr = content.match(/maximum\-dpr=([\d\.]+)/);
if (initialDpr) {
dpr = parseFloat(initialDpr[1]);
scale = parseFloat((1 / dpr).toFixed(2));
}
if (maximumDpr) {
dpr = parseFloat(maximumDpr[1]);
scale = parseFloat((1 / dpr).toFixed(2));
}
}
}

if (!dpr && !scale) {
var isAndroid = win.navigator.appVersion.match(/android/gi);
var isIPhone = win.navigator.appVersion.match(/iphone/gi);
var devicePixelRatio = win.devicePixelRatio;
if (isIPhone) {
// iOS下,对于2和3的屏,用2倍的方案,其余的用1倍方案
if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
dpr = 3;
} else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
dpr = 2;
} else {
dpr = 1;
}
} else {
// 其他设备下,仍旧使用1倍的方案
dpr = 1;
}
scale = 1 / dpr;
}

docEl.setAttribute('data-dpr', dpr);
if (!metaEl) {
metaEl = doc.createElement('meta');
metaEl.setAttribute('name', 'viewport');
metaEl.setAttribute('content', 'initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
if (docEl.firstElementChild) {
docEl.firstElementChild.appendChild(metaEl);
} else {
var wrap = doc.createElement('div');
wrap.appendChild(metaEl);
doc.write(wrap.innerHTML);
}
}

function refreshRem(){
var width = docEl.getBoundingClientRect().width;
if (width / dpr > 540) {
width = 540 * dpr;
}
var rem = width / 10;
docEl.style.fontSize = rem + 'px';
flexible.rem = win.rem = rem;
}

win.addEventListener('resize', function() {
clearTimeout(tid);
tid = setTimeout(refreshRem, 300);
}, false);
win.addEventListener('pageshow', function(e) {
if (e.persisted) {
clearTimeout(tid);
tid = setTimeout(refreshRem, 300);
}
}, false);

if (doc.readyState === 'complete') {
doc.body.style.fontSize = 12 * dpr + 'px';
} else {
doc.addEventListener('DOMContentLoaded', function(e) {
doc.body.style.fontSize = 12 * dpr + 'px';
}, false);
}


refreshRem();

flexible.dpr = win.dpr = dpr;
flexible.refreshRem = refreshRem;
flexible.rem2px = function(d) {
var val = parseFloat(d) * this.rem;
if (typeof d === 'string' && d.match(/rem$/)) {
val += 'px';
}
return val;
}
flexible.px2rem = function(d) {
var val = parseFloat(d) / this.rem;
if (typeof d === 'string' && d.match(/px$/)) {
val += 'rem';
}
return val;
}

})(window, window['lib'] || (window['lib'] = {}));

这种方式,可以精确地算出不同屏幕所应有的rem基准值,缺点就是要加载这么一段js代码,但个人觉得是这是目前最好的方案了。

因为这个方案同时解决了三个问题:

  1. border: 1px问题
  2. 图片高清问题
  3. 屏幕适配布局问题

字体大小问题

对于字体缩放问题,设计师原本的要求是这样的:任何手机屏幕上字体大小都要统一,所以我们针对不同的分辨率(dpr不同),会做如下处理:

1
2
3
4
font-size: 16px;
[data-dpr="2"] input {
font-size: 32px;
}

(注意,字体不可以用rem,误差太大了,且不能满足任何屏幕下字体大小相同)
为了方便,我们也会用less写一个mixin:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.px2px(@name, @px){
@{name}: round(@px / 2) * 1px;
[data-dpr="2"] & {
@{name}: @px * 1px;
}
// for mx3
[data-dpr="2.5"] & {
@{name}: round(@px * 2.5 / 2) * 1px;
}
// for 小米note
[data-dpr="2.75"] & {
@{name}: round(@px * 2.75 / 2) * 1px;
}
[data-dpr="3"] & {
@{name}: round(@px / 2 * 3) * 1px
}
// for 三星note4
[data-dpr="4"] & {
@{name}: @px * 2px;
}
}

(注意:html的data-dpr属性就是之前js方案里面有提到的,这里就有用处了)

根据经验和测试,还是会出现这些奇奇葩葩的dpr,这里做了统一兼容~

用的时候,就像这样:

1
.px2px(font-size, 32);

当然对于其他css属性,如果也要求不同dpr下都保持一致的话,也可以这样操作,如:

1
2
.px2px(padding, 20);
.px2px(right, 8);

  引入flexible.js是为了实现自适应,使移动端布局在不同设备上看起来是一样的,例如在iphone6上一行中可以看到3个div,那么在其他设备上也只能看到3个,通常来说,会让宽高paddingmargin等布局元素使用.px2rem进行转换,这样才能实现上述的效果。对于图片的设置,可以用.px2rem设置外层div宽高后,图片在里层100%;而文字通常来说用px来实现就好,这样在哪个设备上所看到的16px也就是真实的16px。

需要注意的问题是:
  当你要用到dpr和viewport等元素进行scale缩放时,才用到关于字体的.px2px(也就是说flexible.js和.px2px要同时使用,假设设计稿为750*1344,那么在less上写的宽高等也是按照750的设计稿来量,在网页调试状态下,所看到的宽高是在less中写的宽高,但是由于viewport的scale按照比例进行缩小了,所以才呈现了正常的逻辑像素下的效果,此处要理解!),因为这是专门为了设备的dpr不同而准备的,这里引申出来一个问题:
  如果你需要引入其他的css库,例如WeUI微信网页开发样式库,那么当你的页面使用scale进行缩放的时候,就会影响到其他库的使用,所以说,使用需三思。

  当然,你也可以摒弃一些关于dpr的设置,这样就不用加上字体的.px2px去适应不同的dpr了,而页面的可扩展性就更强。你只需要根据设计稿的大小加上.px2rem,并且设置好转换格式就能轻松地实现自适应屏幕了。
  最后给出一张没有布局适配(上图)和用rem布局适配(下图)的对比图,参考代码Github
没有布局适配
rem布局适配
  可以看出,同样的html代码,使用rem布局适配后无论在哪种移动端下,布局都是一样的,而没有使用布局适配的页面就会产生偏差:每一行的图标个数不同。

  如果忽略border: 1px问题这个问题的话,为了不影响其他样式库元素的缩放和方便你理解页面,我把js修改成如下:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
;(function(win, lib) {
var doc = win.document;
var docEl = doc.documentElement;
var tid;
var flexible = lib.flexible || (lib.flexible = {});

function refreshRem(){
var width = docEl.getBoundingClientRect().width;//逻辑像素
if (width > 540) {
width = 540;
}
var rem = width / 10;
docEl.style.fontSize = rem + 'px';
flexible.rem = win.rem = rem;
}

win.addEventListener('resize', function() {
clearTimeout(tid);
tid = setTimeout(refreshRem, 300);
}, false);
win.addEventListener('pageshow', function(e) {
//查看页面是直接从服务器上载入还是从缓存中读取,从浏览器的缓存中读取该属性返回 ture
if (e.persisted) {
clearTimeout(tid);
tid = setTimeout(refreshRem, 300);
}
}, false);

refreshRem();

flexible.refreshRem = refreshRem;
flexible.rem2px = function(d) {
var val = parseFloat(d) * this.rem;
if (typeof d === 'string' && d.match(/rem$/)) {
val += 'px';
}
return val;
}
flexible.px2rem = function(d) {
var val = parseFloat(d) / this.rem;
if (typeof d === 'string' && d.match(/px$/)) {
val += 'rem';
}
return val;
}

//var t1 = flexible.rem2px("10rem");
//var t2 = flexible.px2rem("375px");
//console.log(t1);
//console.log(t2);

})(window, window['lib'] || (window['lib'] = {}));

rem = document.documentElement.clientWidth / 10
上面说过的,乘以dpr,是因为页面有可能为了实现1px border页面会缩放(scale) 1/dpr 倍,所以这里可以不要dpr,这样就不需担心其他样式库的影响了。

总结

  一不小心又写了篇这么长的博客,耐心看完的一定是移动端真爱,答应你,下次写篇短一点的。那么,问题来了,看完后,新技能get到了吗?姐也只能帮你到这了。

参考文章:

  1. 移动端高清、多屏适配方案
  2. 一张图帮你看懂 iPhone 6 Plus 屏幕分辨率
  3. 浅谈前端移动端页面开发(布局篇)
  4. 移动前端
  5. 详解iPhone6分辨率与适配技巧
  6. RESOLUTION INDEPENDENCE Towards A Retina Web
文章目录
  1. 1. 前言
  2. 2. 背景
  3. 3. 一些概念
  4. 4. 几个问题
    1. 4.1. retina下,图片高清问题
    2. 4.2. retina下,border: 1px问题
    3. 4.3. 多屏适配布局问题
    4. 4.4. 字体大小问题
  5. 5. 总结