前言 对于移动端重构,又是一大令人头疼的问题,每次开始一个项目前,我都要认真思考一番使用何种布局方式会比较好,使用什么库比较好,使用的库会不会产生不兼容等种种问题。为了以后的我做项目前可以少一点纠结,我决定总结一下关于移动端如何进行适配的问题。 相关代码示例已经上传到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 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;
这样的缺点,很明显,普通屏幕下:
同样下载了@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 -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 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 = set Timeout(refreshRem, 300); }, false ); win.addEventListener('pageshow' , function (e) { if (e.persisted) { clearTimeout(tid); tid = set Timeout(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 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,这里做了统一兼容~
用的时候,就像这样:
当然对于其他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 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 = set Timeout(refreshRem, 300); }, false ); win.addEventListener('pageshow' , function (e) { //查看页面是直接从服务器上载入还是从缓存中读取,从浏览器的缓存中读取该属性返回 ture if (e.persisted) { clearTimeout(tid); tid = set Timeout(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到了吗?姐也只能帮你到这了。
参考文章:
移动端高清、多屏适配方案
一张图帮你看懂 iPhone 6 Plus 屏幕分辨率
浅谈前端移动端页面开发(布局篇)
移动前端
详解iPhone6分辨率与适配技巧
RESOLUTION INDEPENDENCE Towards A Retina Web