数学方法解释 dpr/scale/rem 三者的关系

开发过移动端页面的同学一定听过 dprscalerem 三个概念。最起码,也会用过 scale,如

因为如果你不设置这一行,几乎所有的移动端浏览器都会把宽度设置为 980px,页面上的文字变得太小而难以阅读。

那么,这三者究竟有着怎样的关系呢?

首先从需求讲起。

移动端设备的屏幕尺寸千差万别,即便设计师能够提供每一种尺寸下的 UE 图,工程师也无法做到针对每种场景的适配。一般地,作为近似,在技术上可以使用媒体查询(media query)的方式将屏幕尺寸划分为几个等级,不同等级下使用不用的CSS样式。

但这显然不够精确,在不同设备上很难做到体验一致,不但代码难以维护,同时存在着被设计师吐槽的风险。

如何在不同的屏幕上完美还原设计图,同时兼顾有限的人力与时间?

由此我们可以提出需求:

  1. 设计师只输出一套基准尺寸的 UE 图;
  2. 通过页面缩放还原 UE 图的设计比例

这有两种方案:

  1. 使用 rem 为单位设置各个元素的尺寸;
  2. 设定一个固定的页面宽度,所有元素可以使用 px 设定尺寸,然后缩放整个页面

我们分开来讲讲这两种方案。

第一种方案,rem

我们经常看到业界的大致方案是:

scale 设置为 1 / dpr<html>font-size 计算为 screen.width * dpr / >10,然后在以 less 将UE图上得到的尺寸透明转换为对应的 rem 值。

由于 rem 是比例值,因此能做到最终每个元素的尺寸相对于 UE 图的比例都是一致的。

那么,问题是,上面的公式是怎么得到的?

分析

我们来用最基本的数据算式推导一下。

设基准 UE 的图宽为 ue_w<html>font-size 值为 ue_fs px

在 PSD 上量得一个元素的宽度为 psd_w px,等于 psd_rem,即:

psd_rem * ue_fs = psd_w -----------(1)

在一个宽度为 foo_w 的设备上,该元素应该给定的宽度为 x_w px

根据 rem 单位的意义可知:

foo_rem * foo_fs / foo_w = psd_rem * ue_fs / ue_w -----------(2)

即:

foo_rem = psd_rem * (ue_fs / foo_fs) * (foo_w / ue_w) -----------(3)

其中 psd_remue_fsfoo_wue_w 皆为已知,而 foo_fs 可给定一个具体值,相当于已知。

1
2
3
.px2rem(@px){
@px * (foo_w) / (foo_fs * ue_w) rem
}

例如,以iPhone6的尺寸为基准,即:

ue_fs = 75px(任取值)
ue_w = 375px

一个宽度为屏幕宽度一般的元素,即:

psd_rem = 375px / 2 / 75px = 2.5rem

在一台 iPhone6 plus 上,则:

foo_w = 414px
foo_fs = 69px(任取值)

代入(3),得

foo_rem = 2.5 * (75 / 69) * (414 / 375) = 3rem = 3 * 69px = 414px / 2

刚好也为屏幕的一半。因此,上面的 LESS 实际内容是:

1
2
3
.px2rem(@px){
@px * 0.016 rem
}

可见,实现与 UE 图等比例的效果,只要定一个基准的 ue_w 和一个基准的 ue_fs,并任取一个当前设备的 foo_fs 就可以了,跟什么 dprscale 根本没有关系。

事实

那么如何根据屏幕宽度取一个合适的 foo_fs 呢?

再来看上面的(3)式,为了更精确的还原UE图,我们一定希望 foo_rempsd_rem 都是有限小数,那么:

(ue_fs / foo_fs) * (foo_w / ue_w)

就也一定是有限小数。分解:

ue_fs / ue_w 

和:

foo_fs / foo_w

最好都是有限小数。因此,只要取屏幕宽度的___约数___做 foo_fs 就可以了,如 iPhone6 上的 75px,iPhone6 plus 上的 69px 等等。

我们知道,在 dpr 大于1的设备上,是画不出来真正 1px 的,除非将 scale 设置成 1 / dpr。这样,foo_w 也会成倍增加:

foo_w = screen.width * dpr

因此为了支持1物理像素,scale 必须设置成 1 / dprfoo_fsscreen.width * dpr 的约数。

CSS3 中新增了 vwvh 两个单位,分别代表可见区域宽高的百分之一,目前浏览器支持程度还不好:

caniuse

为了向后兼容,我们取 foo_w 的十分之一(百分之一会出小数)作为 foo_fs

foo_fs = foo_w / 10 = screen.width * dpr / 10 -----------(4)

这样,dpr 为2时,一个宽度为屏幕一半的元素尺寸为 5rem,或 50vw,仅数量级不同。

这就是为什么业界以(4)式计算 foo_fs 的缘由了。

rem 的方案就讲到这里,已经用数学算式推导出了 foo_fs 的计算公式(4)。

核心代码参考:

1
2
3
4
5
var dpr = window.devicePixelRatio;
var meta = '<meta name="viewport" content="width=' + baseW + ", initial-scale=" + scale + ", maximum-scale=" + scale + ", minimum-scale=" + scale + ', user-scalable=no"/>';
document.write(meta);
var style = '<style tyle="text/css">html{font-size:' + (screen * dpr / 10) + 'px !important}</style>';
document.write(style);

第二种方案,scale

相比于第一种,第二种方案显得简单粗暴:

设置一个基准的尺寸,页面上所有元素都按照此基准布局。然后将页面缩放到设备的真实尺寸上去。

比如设定基准为 400px,而真实设备尺寸为 500px,则 scale 必须为 500 / 400 = 1.25。核心代码参考:

1
2
3
4
var baseW = 400
var scale = screen.width / baseW;
var meta = '<meta name="viewport" content="width=' + baseW + ", initial-scale=" + scale + ", maximum-scale=" + scale + ", minimum-scale=" + scale + ', user-scalable=no"/>';
document.write(meta);

同时还必须要设置HTML的宽度:

1
2
3
html {
width: 400px !important
}

不必再去计算 foo_remfoo_fs 等参数。由于 scale 并非等于 1 / dpr,因此1物理像素也就没法实现了。同时,vwvh 的兼容也没有体现。好在它不需要转换 px 为 rem。

对比

比较上述两种方案:

方案 等比布局 1物理像素 兼容vw/vh 绝对定位 UE尺寸 高清图
第一种 LESS
第二种 有误差

因此两种方案的使用场景是:

  1. 如果必须实现1物理像素,或者需要精确的高清图片,或者对 vw/vh 向后兼容,则使用方案一,代表有淘宝,,缺点是需要单位换算,也存在一定的误差;
  2. 预处理,或者需要精确的像素控制,则使用方案二开发会比较方便,代表有百度H5,缺点是不能实现1物理像素,也不能实现精确的高清图片

回归

上述两个方案比较让人不爽的是都使用了 JavaScript 脚本来动态设置 scale 的大小。在苹果公司的原始设计中, viewport 是这样使用的么?

参见 Safari HTML Referenceviewport 允许开发者设置 width 来调整适配的目标设备宽度,但最终document.documentElement.clientWidth的值为

document.documentElement.clientWidth === Math.max(screen.width / scale, width)

在前面两个方案中,width 等于 screen.widthscale 小于1,因此

document.documentElement.clientWidth === screen.width / scale === screen.width * dpr

如果 scale 写死为1,自定义的 width 不能小于 screen.width。但一旦 width 大于 screen.width,就会出现滚动条,这时,JavaScript 动态计算的 scale 上场了。

因此,按照苹果公司的设计初衷,没办法不使用 JavaScript 实现完美的 UE 还原。使用一份 UE 图,无法做到多个屏幕尺寸上呈现一致的效果。

横屏

移动端开发经常缺失的一个环节是,设计师很少提供横屏版的 UE。当手机屏幕横过来怎么办?

可以监听 resizepageshow 等事件,事件触发后,重新计算 scalefoo_fs 等值。这样能保证页面元素的比例仍与 UE 相当。

有没有问题?

水平方向上好像没什么问题。垂直方向呢?

当整体页面按比例放大后,页面高度必然也会等比放大,而在横屏模式下,屏幕垂直高度又很小,从而导致大部分内容都被推出了首屏,体验和视觉上效果都不好。

因此,元素的高度一般不是用 rem 而使用 px,除非元素尺寸与屏幕尺寸强相关。

这是不是意味着所谓的完美还原是不切实际的?对于那种划页 H5, rem 的高度仍然试用,但对于普通的文本内容则不合适了。

依据具体需求采取不同的方案。

总结

对于普通的需求,width=device-widthscale=1 就够了,虽然不能在不同的设备上展示同样的效果,但是够用。

对展现要求稍高,则使用 JavaScript 动态计算 scalefoo_fs