移动端适配
前言
对于移动端重构,又是一大令人头疼的问题,每次开始一个项目前,我都要认真思考一番使用何种布局方式会比较好,使用什么库比较好,使用的库会不会产生不兼容等种种问题。为了以后的我做项目前可以少一点纠结,我决定总结一下关于移动端如何进行适配的问题。
相关代码示例已经上传到Github。
背景
- 开发移动端H5页面
- 面对不同分辨率的手机
- 面对不同屏幕尺寸的手机
一些概念
三个需要了解的概念:
- PPI: 可以理解为屏幕的显示密度
- DPR: 设备物理像素和逻辑像素的对应关系,即物理像素/逻辑像素
- Resolution: 就是我们常说的分辨率
物理像素与逻辑像素
看了我们上面内容一的第一点之后,或许有些人会有些疑问,我的安卓手机,或者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))
设备像素比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
2width: 200px;
height: 300px;
2.背景图片1
2
3
4width: 200px;
height: 300px;
background-image: url(image@2x.jpg);
background-size: 200px 300px; // 或者: background-size: contain;
这样的缺点,很明显,普通屏幕下:
- 同样下载了@2x的图片,造成资源浪费。
- 图片由于
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
说明:
- 乘以dpr,是因为页面有可能为了实现1px border页面会缩放(scale) 1/dpr 倍(如果没有,dpr=1),。
- 除以10,是为了取整,方便计算(理论上可以是任何值)
所以就像下面这样,html的font-size可能会:
1 | iphone3gs: 320px / 10 = 32px |
对于动态改变根节点html的font-size,我们可以通过css做,也可以通过javascript做。css方式
,可以通过设备宽度来媒体查询来改变html的font-size:
1 | html{font-size:32;} |
缺点:通过设备宽度
范围区间
这样的媒体查询来动态改变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代码,但个人觉得是这是目前最好的方案了。
因为这个方案同时解决了三个问题:
- border: 1px问题
- 图片高清问题
- 屏幕适配布局问题
字体大小问题
对于字体缩放问题,设计师原本的要求是这样的:任何手机屏幕上字体大小都要统一
,所以我们针对不同的分辨率(dpr不同),会做如下处理:1
2
3
4font-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个,通常来说,会让宽高
和padding
,margin
等布局元素使用.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:
可以看出,同样的html代码,使用rem布局适配后无论在哪种移动端下,布局都是一样的,而没有使用布局适配的页面就会产生偏差:每一行的图标个数不同。
如果忽略border: 1px问题
这个问题的话,为了不影响其他样式库元素的缩放和方便你理解页面,我把js修改成如下:
1 | ;(function(win, lib) { |
rem = document.documentElement.clientWidth / 10
上面说过的,乘以dpr,是因为页面有可能为了实现1px border页面会缩放(scale) 1/dpr 倍,所以这里可以不要dpr,这样就不需担心其他样式库的影响了。
总结
一不小心又写了篇这么长的博客,耐心看完的一定是移动端真爱,答应你,下次写篇短一点的。那么,问题来了,看完后,新技能get到了吗?姐也只能帮你到这了。
参考文章: