移动端适配-实践篇

3,936 阅读15分钟

这是一个系列文章,分 3 篇:

一般我们在做前端项目之前,都会先拿到视觉稿和交互稿,我们可以根据视觉稿上的尺寸、颜色等信息编写 CSS 样式,可以根据交互稿来写 JS。

每个公司的视觉规范不同,视觉稿也就不同。甚至在一些大公司里,每个部门都有自己的视觉规范。比如页面画布大小是以 640 为基准还是 750。最终给前端开发人员的文件也可能不同,比如 PSD 文件、Sketch 文件或者图片文件,其中包括页面文件和切图。

我做过的项目里,有这几种情况:

  • Sketch文件包 + 总体设计图 + 每个页面图片 + 切图(@2x、@3x 各一份)
  • PSD 源文件 + 总体设计图 + 每个页面图片(有的带尺寸/颜色标注) + 切图(@2x、@3x 各一份)
  • 只给带标注的图片的,这种我都会问UED要源文件,标注很难画全的。

我们 UED 画布基准都是 iphone6,但是有的是给的画布宽 375 的 Sketch 文件,有的给画布宽 750 的 PSD 文件。

下面以我做过的一个页面为例,左边是Sketch 文件的截图,右边是 PSD 文件导出的图片。

这个页面涉及到了移动端适配的几个问题:

  • 布局适配,不同屏幕尺寸的设备中布局一致
  • 图片高清适配,不同分辨率设备中图片高清展示
  • 不同分辨率设备中 1px 边框显示一致
  • 内容适配,不同屏幕尺寸的设备中的文字大小

那,拿到视觉稿了,我们要开始写代码了。

假如我们拿到的是宽 750 的视觉稿
**
在写 CSS 代码前还得做件事儿,手机端布局视口默认情况下是 768px ~ 1024px 之间,手机屏幕宽度大家知道的iphone5 320px, iphone6 375px, iphone6 plus 414px 这样子。

像下面图示这样,在手机上浏览页面时得横向滚动,是不是很不友好?



所以我们得想办法,让页面横向内容都展示在屏幕可视范围中,并且禁止缩放。有两种办法:
  • 设置布局视口宽度为一个定值,然后按照布局视口的宽度与屏幕宽度的比例缩放
  • 设置布局视口的宽度 = 设备宽度

设置布局视口宽度为一个定值,按照布局视口的宽度与屏幕宽度的比例缩放

比如屏幕宽 375px,视觉稿 750px,那就设置布局视口的宽度 width=750,
scale = 375 / 750 = 0.5

<meta name="viewport" content="width=750,initial-scale=0.5,maximum-scale=0.5,user-scalable=no">

再比如屏幕宽 320px,视觉稿 750px,那就设置布局视口的宽度 width=750,
scale = 320 / 750

当然这是需要根据屏幕宽度动态设置的:

(function(){
	var doc = window.document;
	var metaEl = doc.querySelector('meta[name="viewport"]');
	if(!metaEl){
		metaEl = doc.createElement("meta");
		metaEl.setAttribute("name", "viewport");
	}
	var metaCtt = metaEl ? metaEl.content : '';
	var matchWidth = metaCtt.match(/width=([^,\s]+)/);
	var width = matchWidth ? matchWidth[1] : 750
	if(width == 'device-width'){return}
	
	var screenWidth = window.screen.width;
	var scale = screenWidth/width;
	metaEl.setAttribute("content", "width="+ width +",user-scalable=no,initial-scale=" + scale + ",maximum-scale=" + scale + ",minimum-scale=" + scale);
})()

demo1
下面是依次在 iphone6 plus,iphone6,iphone5 上的展示效果。

image.png

那有人就会问啦,给的视觉稿是基于 iphone5 的,画布宽度 640 怎么办?
**
那 CSS 编程就按照视觉给的来,width 设置 640,在宽 640 的布局视口,CSS 样式编写与视觉稿相同,那布局就能与视觉稿相同啦。再通过动态缩放就能把整个页面完整的展示在可视窗口中啦。

那又有人问啦,给的不是 750 宽的视觉稿,是 Sketch 视觉稿,宽 375,这怎么办?
**
一样的道理,视觉稿宽 375,视觉稿中的元素都是按宽 375 来布局的,那我们布局视口 width 设置 375,CSS 样式按375的视觉稿来写就好啦,最后就是动态缩放啦。

好啦,那我们来看这种方式能不能解决我们遇到的几个问题。

图片高清问题

**
实际要解决的是怎么让 1 个位图像素正好覆盖 1 个物理像素,这样可以让图片在不同 dpr 的手机上效果一样。

  • dpr = 1,需要 1 倍图
  • dpr = 2,需要 2 倍图
  • dpr = 3,需要 3 倍图

最好的解决办法是:不同的 dpr 下,加载不同的尺寸的图片

1px 边框问题

**
实际要解决的是怎么让 1 个 CSS 像素正好覆盖 1 个物理像素,这样可以让 1px 边框在不同 dpr 的手机上一样细。
但是布局视口缩放比为 1/dpr 才能让 1 个 CSS 像素是否正好覆盖1个物理像素。

显然这种按照布局视口的宽度与屏幕宽度的比例缩放的方式,只有与视觉稿画布基准一致的设备才能与视觉稿要求显示的一致。比如这里只有在 iphone6 上 1 个 CSS 像素是否正好覆盖1个物理像素(横向 200px 的元素对应 400 个物理像素,在缩放 0.5 后,横向 200px 的元素对应 200 个物理像素,1:1)

布局适配的问题

其实直接从上面的对比图就可以看出来,在不同设备上布局是一致的。
原理很简单: 我们写 CSS 本来就是按视觉稿来写的,然后整体缩放,以适应各种宽度的设备,元素的比例没有变。

内容适配的问题

主要是文字大小,因为是整体缩放,屏幕越大,字体也越大。
这个主要看视觉规范怎么定,有的觉得这样挺好,有的就希望字体大小都一样,还有的希望字体大小不是根据屏幕大小按比例缩放而是对一定范围内的屏幕宽度设定特定的字体大小。

总结

【原理】

设定布局视口宽度为视觉稿画布宽度,动态设置缩放比例scale=屏幕宽度/视觉稿画布宽度

【优点】

实现简单,可解决不同屏幕大小的布局问题,在各种屏幕上布局一致

【缺点】

  • 不能解决 1px 边框问题;
  • 缩放值依赖于屏幕宽度,demo 里是通过 screen.width 获取屏幕宽度的,这在 chrome 里 iphone6 返回的是屏幕宽度值 375,但在其他浏览器就不一定了,比如safari中返回的是 1280。
  • 所有元素都会缩放,比如字体,在 iphone5 上就会小很多,这不一定是大家想要的。

设置布局视口的宽度 = 设备宽度

<meta 
      name="viewport" 
      content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">

这种情况下有什么效果呢?

demo2

图片太长,横着放了^^

image.png

看看看,都被挤掉了,为啥?
这个是 iphone6 的截图,现在设置了布局视口宽度是屏幕宽度 375,我们视觉稿是按 iphone6 二倍图来的宽 750,那当然就放不下了,比如视觉稿中图片占位 400px x 400px,我们 CSS 中写的:

img{
	width: 400px;
	height: 400px;
}

但是布局视口宽度就 375,图片就放不下了。
那 iphone6 布局视口宽只有 375,我们把视觉稿尺寸全部按比例缩小到 375 就好啦。那 750 到 375,需要除以 2,我们在编写 CSS 样式时除以 2 就好啦。

上面视觉稿中图片是 400 x 400,我们 CSS 中就写:

img{
	width: 200px;
	height: 200px;
}

其他元素的宽高、边框、内边距、外边距、字体大小等,通通先除以 2,通通先除以 2。那现在的效果是这样的,依次是 iphone6 plus,iphone6, iphone5

image.png

现在,我们能看到 100% 呈现在可视窗口中了。那再来看看我们遇到的问题解决了吗?

**1px 边框问题

设置 0.5px,在 dpr=2 的设备中对应 1 个物理像素,在 dpr=3 的设备中对应 1.5 个物理像素。
而且在 dpr=2 的设备中设置 0.5px 不一定能达到我们想要的效果,看下面:

image.png

明明设置的 0.5px,但浏览器实际还是按 1px 处理的。ios7 以下,android 等其他系统里,0.5px 会被当成为 0px 处理? 所以直接设置成 0.5px 是不可行的。 这实际上还是需要 1 个 CSS 像素正好覆盖 1  个物理像素就可以解决的。

**布局适配的问题

因为没有按设备属性动态缩放,每个设备上每个css像素大小都一样,200px 大小的元素在每个设备上也够一样长。
其实从上面的对比图就能看出来,图片容器(灰色背景)在不同屏幕大小的手机都是一样的高,整个页面在小屏手机中就需要向下滚动查看,一排展示的 4 个图片标签在大屏手机中右边的空白就比较大。布局问题也没有解决。

**内容适配的问题 **
文字大小统统一样,也没有解决。

1 个 css 像素只占 1 个物理像素

前面已经讲了几遍了,要解决 1px 边框的问题,达到在不同分辨率设备上效果一致(当然也与视觉稿一致),需要让不同分辨率设备上  1 个 css 像素只覆盖 1 个物理像素

前面我们将 750 视觉稿中的尺寸除以 2 来编写样式,只能在 iphone6 上的效果满足。那现在我们在布局宽度=屏幕宽度时,让缩放比例为 1/dpr,就可以让 1 个 css像素只覆盖1个物理像素

image.png

比如 750 的视觉稿,元素边框 1px,我们 CSS 样式也写:

div{
	border-width: 1px;
}

然后 JS 动态设置缩放逻辑大概是这样子:

var doc = document;
var docEl = document.documentElement;
var isIos = navigator.appVersion.match(/(iphone|ipad|ipod)/gi);
var dpr = window.devicePixelRatio || 1;
if (!isIos){ dpr = 1 }
var scale = 1 / dpr;
var metaEl = doc.querySelector('meta[name="viewport"]');
if (!metaEl) {
    metaEl = doc.createElement("meta");
    metaEl.setAttribute("name", "viewport");
    doc.head.appendChild(metaEl)
}
metaEl.setAttribute("content", "width=device-width,user-scalable=no,initial-scale=" + scale + ",maximum-scale=" + scale + ",minimum-scale=" + scale);

那现在无论什么分辨率的设备,1个css像素实际只占1个物理像素了。
那现在效果是什么样子呢?

demo3

image.png

所有元素尺寸都缩小 1/dpr 了,本质上是 1 个 CSS 像素宽度缩小了 1/dpr,不同 dpr 的设备上 CSS 像素大小就不同了。那我们 CSS 样式 200px,在不同 dpr 的设备上所占宽度也不同了。

注意: 如果在安卓设备上查看 demo3,与 demo2 的效果相同,因为 dpr 都被处理成 dpr=1,没有缩放,CSS 样式又与 750 视觉稿一致。所以这种方式只缩放,不控制布局是不行的。
**
这个问题可以在待会讲布局适配时解决,布局适配的目的是让元素在不同设备上占比一致(与视觉稿相同。)

解决布局适配问题

可以看下demo3的图

  • 不同屏幕大小的手机都是一样的高,在小屏手机中就需要向下滚动查看。
  • 选择图片标签下一行展示4个,但在大屏手机中右边的空白就比较大。

那我们希望,元素在布局视口中的占比(包括元素宽高、边距等)在不同设备上都相同
同一份样式怎么让元素占比相同呢?用绝对单位 px 肯定是不行的,我们考虑相对单位 rem。

假如有个宽可以表示为 20rem 的元素:

CSS 样式 CSS 像素个数
width:20rem 20 * html元素的font-size
元素占比 = 元素 CSS 像素个数 / 布局视口宽度
        = 20 * html元素的font-size / 布局视口宽度

所以只要满足:

常量值 *  HTML 元素的 font-size =  布局视口宽度

元素占比在任何设备上都是某一个定值了。
那我们只要按下面的公式动态设置 HTML 元素的 font-size 就好啦。

HTML 元素的 font-size = 布局视口宽度 / 常量值

这个常量值可以是任意一个常数,但为了写样式方便,我们会做一些考虑,例如:

为了解决前面 1px 边框 retina 屏显示的问题,页面缩放了 1/dpr,iphone6 布局视口的宽度就是:375 * 2 个 css 像素
我们可以让 iphone6 中 HTML 元素的 font-size = 375 * 2 / 7.5 = 100,这样可以直接将视觉稿中的尺寸除以100,得到 rem 单位的数值

对于以 iphone6 为基准的 750 视觉稿,并且缩放 1/dpr,HTML 元素的 font-size 与 布局视口宽度视口的比例可以是:

HTML 元素的 font-size = 布局视口宽度 / 7.5

对于以 iphone5 为基准的 640 视觉稿,并且缩放 1/dpr

HTML 元素的 font-size = 布局视口宽度 / 6.4

大概的实现就是这样子:

var doc = document;
var docEl = document.documentElement;
var isIos = navigator.appVersion.match(/(iphone|ipad|ipod)/gi);
var dpr = window.devicePixelRatio || 1;
if (!isIos){ dpr = 1 }
var scale = 1 / dpr;
var metaEl = doc.querySelector('meta[name="viewport"]');
if (!metaEl) {
    metaEl = doc.createElement("meta");
    metaEl.setAttribute("name", "viewport");
    doc.head.appendChild(metaEl)
}
metaEl.setAttribute("content", "width=device-width,user-scalable=no,initial-scale=" + scale + ",maximum-scale=" + scale + ",minimum-scale=" + scale);
setTimeout(function(){
	var width = docEl.getBoundingClientRect().width;
	docEl.style.fontSize = width / 7.5 + 'px';
})

因为 scale=1/dpr && 基于 750 宽的视觉稿,元素的CSS 样式(rem)可以直接由视觉稿尺寸除以 100
最后的效果是:

demo4

HTML 元素的 font-size = 布局视口宽度 / 10

demo5

HTML 元素的 font-size = 布局视口宽度 / 7.5

image.png

那有人会问啦,我的视觉稿是基于 iphone6 的,但给的是一倍图宽 375 怎么办?
**
视觉稿一般有两种类型: 一倍图 和 二倍图,比如基于 iphone6 的:

画布宽 750,元素 400 x 400
画布宽 375,元素 200 x 200

缩放一般有两种:

scale = 1,布局视口宽度 = 375 个 CSS 像素
scale = 1/dpr,布局视口宽度 = 750 个 CSS 像素

如果视觉稿选择画布宽 750,并且 scale=1,那元素 CSS 像素应该由视觉稿中的尺寸除以 2,使布局视口宽度 375 中 400/2 x 400/2 与 视觉稿宽 750 中 400 x 400 的元素比例一致

如果视觉稿选择画布宽 375,并且 scale=1/dpr,那元素 CSS 像素应该由视觉稿中的尺寸乘以 2,使布局视口宽度 750 中的元素 200x2 x 200x2 与 视觉稿宽 375 中 400 x 400 的元素比例一致
其他情况,元素 CSS 像素值就是视觉稿中的值。

知道这个之后,再决定 px 与 rem 的换算值:HTML 元素的 font-size,然后再写 CSS 样式(rem)。
举几个例子:

如果视觉稿选择画布宽 750,并且 scale=1,HTML 元素的 font-size = 375 / 7.5 = 50,400/2/50= 4rem

img{
    width: 4rem;
    height: 4rem;
}

如果视觉稿选择画布宽 375,并且 scale=1/dpr,HTML 元素的 font-size = 750 / 7.5 = 100,200 * 2 /100 = 4rem

img{
	  width: 4rem;
	  height: 4rem;
  }

画布宽 750 & scale=1/dpr,HTML 元素的 font-size = 750 / 7.5 = 100,400 / 100 = 4rem

img{
	  width: 4rem;
	  height: 4rem;
  }

画布宽 375 & scale=1,HTML 元素的 font-size = 375 / 7.5 = 50,200 / 50 = 4rem

img{
	  width: 4rem;
	  height: 4rem;
  }

总结

1、设置 HTML 元素的 font-size 与布局视口宽度成比例,CSS 样式使用相对单位 rem,元素尺寸也就与布局视口宽度成比例,从而在每个设备布局一致。用视觉稿来确定这个比例值就能与视觉稿的布局一致。

2、 设置缩放比例为 1/dpr 可以解决 1px 边框问题。
 
为了 rem 与 px 换算方便,选定的比例值可以让 HTML 元素的 font-size 为 100px。
iphone6 就是 7.5,iphone5 就是 6.4,不同基准的视觉稿 rem 与 px 换算比例都是 100.
最后,如果不想动态适配的内容,可以直接使用 px 单位,比如字体,还有1px边框。

淘宝和网易的做法

这里讲的适配方案是今天 [2017-08-11] 的,以后网站方案可能会有改动,所以我把代码保存下来了,可以看适配代码
**

手机淘宝

淘宝统一定为 10,iphone6 是 1rem = 75px,iphone5 1rem = 64px,可以到手机淘宝验证。不同基准的视觉稿 rem 与 px 换算比例不同。

网易

并没有做缩放,只使用了相对单位 rem。

HTML 的 font-size = 布局视口宽度/7.5 = 设备屏幕宽度 / 7.5

在 iphone6 上,HTML 的 font-size 为 375/7.5 = 50px,如果他们的视觉稿是 1 倍图,那就是直接除以 50 来写 CSS 的,如果是二倍图,就是除以 100 来写 CSS 的。

这个比例可以随便设置,根据实际的需求,网易设置 7.5 时考虑换算简单,淘宝设置 10 则是考虑兼容 vw。

总结

动态修改 HTML 元素的 font-size 和动态修改 viewport 在很多安卓设备上有兼容性问题。现在越来越多的浏览器支持 vw、vh,viewport+rem 的方案不一定是最好的方案。

对于 1px 边框的问题,也有很多其他的解决方案。

  • 判断终端类型是否支持 0.5px,支持则直接定义边框为 0.5px
  • 使用 CSS 定义样式:-webkit-transform: scale(0.5),对边框进行缩放
  • 使用 CSS 实现阴影代替边框:-webkit-box-shadow:0 1px 1px -1px rgba(0, 0, 0, 0.5)
  • 使用 CSS 定义背景样式(background-image)来实现边框