移动开发不同与PC端开发,可能会经历各种意想不到的问题,尤其是移动端应用刚起步的几年;随着移动互联网的快速发展,有些问题已经得到了很好的支持,如1像素边界的问题。当然,要更好地解决这些移动端的问题,就需有移动端领域相关的知识,下面就来说说。
dpr设备像素比
首先说一下,这个dpr不仅仅是移动端才有的,pc端也有,但是对一些移动端的问题产生的原因及解决显得比较重要,比如1像素的问题。先来看几个概念:
-
物理像素(physical pixel)
一个物理像素就是显示设备上最小的物理显示单元,每个物理像素都有自己的颜色值和亮度值。例如iphone6手机屏幕有750*1334个物理像素
-
设备独立像素(density-independent
设备独立像素又叫密度无关像素,也可以叫逻辑像素,程序使用的虚拟像素如css像素,可以理解为显示设备坐标系统中的一个点;
-
设备像素比dpr(device pixel ratio)
设备像素比,简称dpr,定义了物理像素与设备独立像素之间的对应关系,具体的对应关系是一个计算公式如下:
dpr = 物理像素 / 设备独立像素
上面计算的dpr是指某一个方向上如x或者y方向,二者dpr值相同;程序中获取dpr方式如下:
js获取dpr使用
window.devicePixelRatio
css获取dpr使用
-webkit-device-pixel-radio
例如iphone6,设备宽高375 * 667,可以理解为设备独立像素(也即css像素);其dpr为2,那么对应的物理像素宽高均 * 2,即 750 * 1334;也就是说一个逻辑像素,在x轴和y轴都需要2个物理像素来显示,一图胜千言,如图:
由上线描述可以知道,css中的1px并不等于显示设备的物理1px,这就导致移动开发中设计师设计的是1px的物理像素,而转换为css的值为1/dpr,其可能为小数值,这在低版本android(<=4.4)和ios(<=8)中被会系统自动转换为0,这就是移动端常见的1px像素的问题;下面会给出具体的解决方案。
viewport
做过移动端开发的同学可能对下面html中的meta标签比较熟悉:
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no" >
这个是用来控制移动端viewport
区域是怎么展现的,很有必要对其理解。
viewport理解
在PC的浏览器中,viewport其实就是浏览器可视区域,但是在移动设备上问题就比较复杂,viewport并不局限于浏览器可视区域大小,可能比浏览器可视区域要大,也可能比浏览器可视区域要小;但是因为移动端屏幕相较pc端太窄,为在移动端正常显示为PC端设计的网站,默认情况下移动端设备上的viewport都是要大于浏览器可视区域的,一般值为980px
也可能有其他值,根据不同设备来定;因为viewport比浏览器可视区域大,那么浏览器就会出现横向滚动条。
需要注意两点:
页面的html标签的宽度就是相对于viewport的大小。
PC端viewport就是浏览器可视区域大小;移动端默认viewport值为980px,也可以根据meta标签自定义设置。
有关viewport理论,国外有一个人ppk对此有做过比较深入的研究,具体可以参考其写的三篇文章①、②、③ 。其将viewport分成三个层面来理解:
-
layout viewport
布局窗口,网页真正的布局视口,它的宽度可以大于也可以小于浏览器可视区域的宽度,对于大于浏览器可视区域(比如默认980px的viewport或者自定义设置viewport)的viewport,只能通过滚动浏览器滚动条来展现其内容。
-
visual viewport
可视窗口,移动设备浏览器可视区域的大小,其宽度并一定为移动显示设备的屏幕宽度,在initial-scale缩放为1的情况下才相等。
-
ideal viewport
理想化的窗口,它没有一个固定的尺寸,其宽度为移动设备屏幕宽度,它是最适合移动设备的viewport。设置理想化的窗口的网页不论何种分辨率的屏幕下,其用户不需要缩放和横向滚动条就能正常查看网站所有内容,保证网页的文字、图片等等其大小完美的呈现给用户。
一图胜千言,借用网上的几张图来说明具体的区别:
上面两幅图很好理解,下面两幅对比图,说明ideal viewport对于网站用户体验的重要性,用户不用缩放或者滚动就能达到极佳的体验效果。
用meta标签控制viewport
用<meta name="viewport" content="xxx">
标签来控制viewport大小首先是由苹果公司在其safari浏览器中引入的,目的就是解决移动设备的viewport问题。后来安卓以及各大浏览器厂商也都纷纷效仿,引入它对viewport的支持。
viewport是通过meta
标签来控制页面的viewport大小,具体形式如下:
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no" >
其中width=device-width
顾名思义是设置viewport的宽为设备的宽,其还可以设置具体的逻辑像素值,如980。
meta标签的viewport相关配置content值有6个属性,它们可以同时使用,也可单独使用,还可以混合使用,具体如下:
width | 设置layout viewport 的宽度,为一正整数,也可为device-width
|
---|---|
height | 设置layout viewport 的宽度,为一正整数,很少使用 |
initial-scale | 设置页面的初始缩放值,为一数值,可带小数 |
minimum-scale | 设置页面的最小缩放值,为一数值,可带小数 |
maximum-scale | 设置页面的最大缩放值,为一数值,可带小数 |
user-scalable | 页面是否允许缩放,值为"no"或"yes", no 不允许,yes允许 |
需要注意两点:
user-scalable=no禁止缩放在ios>=10系统的safari下有兼容问题,具体可以看禁止页面缩放meta标签兼容性问题这部分
meta属性中initial-scale的缩放是相对于
ideal viewport
来进行的
这样通过meta
标签很容易设置页面layout viewport为移动设备屏幕宽度(它也是ideal layout)即:
<meta name="viewport" content="width=device-width">
上面这种方式是最直接也是最轻易想到的设法,但是还可以使用initial-scale来达到同样的效果,即:
<meta name="viewport" content="initial-scale=1">
简单解释下,initial-scale
表示页面初始缩放值,其缩放是相对于ideal viewport的来说的,为1表示不缩放,那么其layout viewport的宽度就是ideal viewport的宽度,也就是移动设备屏幕的宽度。
既然二者都可以设置layout viewport的宽度,那么二者同时设置且值不相等会怎样呢?答案是:
浏览器会以二者中值较大的那个为准。
可能读者会有新的疑惑,在设置layout viewport为屏幕宽度时,经常看到的是二者都写上,为什么呢?答案是:
一个是兼容性的考虑,另一方面解决某些设备横竖屏不分导致通通以ideal viewport的宽度为准的问题
viewport的缩放
上面提到,meta中的initial-scale是相对于ideal viewport进行缩放的,该属性的作用:
initial-scale用来确定visual viewport即浏览器可视区域宽度大小
阿里早期的iphone/ipad下的自适应布局解决方案flexible
就是利用initial-scale来解决的。
有人可能会有疑问,移动端浏览器可视区域宽度不就是移动设备屏幕的宽度么?其实我们这里所说的可视区域宽度是逻辑意义上的宽度,而非实际真实的宽度,例如iphone4的320px屏幕宽度,initial-scale放大2倍,那么可视区域的逻辑大小变成了160px,可以通过查看页面html元素的宽度测试。
其实visual viewport与ideal viewport的关系如下:
visual viewport = ideal viewport / initial-scale
在iphone/ipad下,在没有指定initial-scale的情况下,无论你怎么设置layout viewport宽度,它会根据上面的计算关系,自动计算当前页面的inital-scale值以保证layout viewport宽度在缩放后就是浏览器可视区域的的宽度,即inital-scale = ideal viewport宽度 / visual viewport宽度(等于layout viewport宽度)。
例如,iphone6情况下默认的layout viewport为980px,那么当前缩放值inital-scale=375 / 980。
需要说明一下的是:在设置了initial-scale的情况下,这个自动计算的值就不起作用了。
移动适配方案
移动端设备不同,其屏幕大小也不尽相同,那么针对特定移动设备的页面设计ui怎么在不同移动设备上因设备不同而自适应屏幕展示呢。一般常见的解决方案有rem和vw/vh。下面就来说说。
rem适配
rem
是一个相对单位,相对于<html>
的font-size
来说的;那么参与页面布局的单位使用rem
而不是px,这样我们只需控制不同设备下网页<html>
的font-size
即可做到页面的自适应。
可以像flexible一样将移动屏幕宽度100等份,每份为a,同时1rem认定为10a;例如750px的设计稿来说,这样的话:
1a = 750px / 100 = 7.5px
1rem = 10a = 75px
实现代码如下:
(function (doc, win) {
var docEl =doc.documentElement,
resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize',
recalc = function () {
var width = docEl.getBoundingClientRect().width;
if (!width) return;
docEl.style.fontSize = (width / 100)*10 + 'px';
};
if (!doc.addEventListener) return;
win.addEventListener(resizeEvt, recalc, false);
recalc();
// hack兼容某些特殊机型
doc.addEventListener('DOMContentLoaded', recalc, false);
})(document, window);
另外,我们也可以基于特定的设计稿尺寸来计算其他移动设备下的font-size,例如基于750px的设计稿,我们假设其font-size为40px,那么其他设备下font-size的计算关系式:
\[ 750/40 = 设备屏幕宽/fontsize \]
对应的核心转换代码即: docEl.style.fontSize = (width / 750)*40 + ‘px‘
VW适配
首先要知道vw和vh的概念代表什么,它也是相对单位,相对于屏幕的宽高而言的。
100vw = ideal viewport 宽
100vh = ideal viewport 高
跟rem类似,我们可以使用vw单位作为css的唯一单位,这样所有元素基于vw来布局;基于特定设计稿的尺寸来转换vw单位,我们使用stylus预编译函数来进行转换:
$vw_base = 375
vw(px) {
(px/$vw_base) * 100vw
}
然后无论是文本还是布局高宽、间距等都使用 vw 作为 CSS 单位,如
.container
padding: vw(15) vw(10) vw(10)
height: vw(40)
vw+rem结合适配
单纯的vw适配在视口缩放时尤其是缩小时有些小瑕疵,因为vw是利用视口单位实现的布局,依赖视口的大小而自动缩放,也就失去了最大最小的宽度限制。一种比较好的解决方法是使用vw与rem配合来进行适配,即:
页面需要适配的元素使用rem为单位,而
的font-size值是根据vw来设定的,但是该font-size值需要限制最大最小值。
具体的stylus代码如下,也可以参考这个demo猛戳
$fontsize = 75 // 将屏幕分成10份
$base = 750 // iphone6 750作为基数
rem(px) {
(px/$fontsize) * 1rem
}
html
font-size: ($fontsize / $base) * 100vw //font-size以vw为单位
// 通过medai query限制根元素的最大值最小值
@media screen and (max-width: 320px) {// 页面宽度<=320时生效
font-size: 64px
}
@medai screen and (min-width: 540px) { // 页面宽度>=540时生效
font-size: 108px
}
媒体查询media query适配
通过类似如下形式来实现适配:
/* 大于1200px */
@media screen and (min-width:1200px){}
/* 大于等于960px,小于1200px */
@media screen and (min-width: 960px) and (max-width: 1199px){}
/* 大于等于768px,小于960px */
@media screen and (min-width: 768px) and (max-width: 959px){}
/* 大于等于480px,小于768px */
@media only screen and (min-width: 480px) and (max-width: 767px){}
/* 小于479px */
@media only screen and (max-width: 479px){}
该方式比较简单,成本低,但是代码量大,比较臃肿,维护不方便,不推荐该方式。
阿里flexible适配
该方案随着viewport单位
得到众多浏览器的兼容支持已逐渐不推荐使用了,它主要是为了解决iphone系列适配问题;虽然官方已不推荐使用,但是其思想还是值得借鉴学习的,主要表现下面三个方面:
根据
dpr
的值来修改<meta>
的viewport
的值实现移动端1px
的问题根据
dpr
的值来修改<html>
的font-size
值,使用以rem
为单位值来等比例缩放使用hack手段用
rem
模拟vw
特性
对应第一点,它通过hack手段来根据设备的dpr
值相应改变<meta>
标签中的viewport
的值:
<!-- dpr = 1-->
<meta name="viewport" content="initial-scale=scale,maximum-scale=scale,minimum-scale=scale,user-scalable=no">
<!-- dpr = 2-->
<meta name="viewport" content="initial-scale=0.5,maximum-scale=0.5,minimum-scale=0.5,user-scalable=no">
<!-- dpr = 3-->
<meta name="viewport" content="initial-scale=0.3333333333,maximum-scale=0.3333333333,minimum-scale=0.3333333333,user-scalable=no">
这样,iphone系统的不同设备下页面达到的效果是使css 1px像素与物理1px像素相同;然后,flexible使用rem作为布局单位实现适应布局。关键基本代码如下:
// 设置meta的viewport内容进行缩放
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;
// 确定html的font-size值
var width = docEl.getBoundingClientRect().width;
if (width / dpr > 540) {
????width = 540 * dpr;
}
var rem = width / 10; // 屏幕均分100份,每份为a,1rem为10a
docEl.style.fontSize = rem + 'px';
1px边框问题及解决方案
产生1px边框的问题其实归结三点:
1px的css逻辑像素不等于1px的物理像素
ios<8以及Android<=4.4以下的浏览器处理0.5px时会转化为0
对于1px边框问题的解决,flexible
能完美的解决,思路见上面分析的;除此之外还有什么方式,其实网上有很多方法,但是还是列一下思路:
-
用border-image来实现
使用一张图片来充当border,图片形式是3x3,如下:
.border { border-width: 1px; border-image: url(border.png) 2 repeat; }
缺点:改边框颜色时要修改图片,不灵活
-
用背景渐变来实现
设置1px的渐变背景,50%有颜色,50%透明
.border { background-image: linear-gradient(180deg, green, green 50%, transparent 0); background-size: 100% 1px; /* 背景宽度100%,高度1px */ background-repeat: no-repeat; background-position: bottom; }
缺点:维护过多代码,圆角没法实现
-
用box-shadow模拟边框来实现
.border { border: none; height: 100px; width: 100%; box-shadow: 0 1px 1px -1px rgba(0, 0, 0, 0.5); }
缺点:颜色不好处理,有阴影出现
-
用伪类+transform来实现
比较推荐的方法,原理是把原先元素的border去掉,然后利用
:before
或者:after
重做border,并把transform
的scale
缩小一半,原先元素相对定位,伪元素模拟的border采用绝对定位。.border { position: relative; border: none; } .border:after { content: ' '; position: absolute; left: 0; background: #666; width: 100%; height: 1px; tranform: scaleY(0.5); transform-origin: 0 0; }