如果你是一个开始接触移动 Web 开发的前端工程师,那么你或许也遇到了和我曾经遇到的过问题:有太多新的概念需要掌握,太多相似的概念需要区分。没关系,我将用两篇文章的篇幅来解决这些问题。上篇文章关于解释和区分一些入门级别
概念。这些概念你或许一直在各种场合看到或者听说,好像熟的很,但你真的了解它们背后的含义吗?下篇文章我们就需要用到这些概念,聊一聊移动设备上的图片加载方案。至于响应式设计,已经有太多写的非常好的文章来叙述它们,这次在这里我就不赘述了。
我们先从听说最多一个概念——PPI 开始。
PPI
什么是 PPI
PPI 的复杂之处在于如果他所属的上下文环境不同,意义也会完全不一样。 当我们在谈论显示设备的 PPI 时,它代指的屏幕的像素密度;当我们在谈论和图片相关时,我们谈论的是打印时的分辨率或者打印机的打印精度。这里我们主要描述的前一种情况,关于后两种,我们在文章的最后会谈到,有兴趣的同学可以阅读。
PPI 全称为 Pixel Per Inch,译为每英寸像素取值,更确切的说法应该是像素密度,也就是衡量单位物理面积内拥有像素值的情况。
如上图所示,在 1 英寸单位内面积内拥有的像素越多,密度越大,PPI 值就越高。但像素密度的实际意义是什么?它表达的是什么?或高或低对设备显示来说有什么影响?
一般来说,我们当然希望 PPI 值越高越好,因为更高的 PPI 意味着在同一实际尺寸的物理屏幕上能容纳更多的像素,能够展现更多的画面细节,也就意味着更平滑的画面。原理如下:
什么是 Pixel
但你有没有仔细相关 Pixel Per Inch 中的 pixel 像素的概念究竟指的是什么样的像素?你可能会反问我像素难道还分很多种不成?我可以很确定的告诉你,是的。
设备像素
无论是早期的 CRT 显示器还是如今的 LCD 显示器,都是基于点阵的。也就是说通过一些列的小点排列成一个大的矩形,不同的小点通过显示不同的颜色来显示成图像。比如下图就是 LCD 显示器上一个 6x6 个小点排列成的矩阵:
注意每一个像素(pixel,也可以称之为 dot)又是由三个子像素 (subpixel) 红绿蓝组合而成。当需要显示图片信息时,它的工作原理可以如下图所示:
上图中的左侧是放大之后我们能看到的像素,而右侧就是对应像素在显示器上的显示情况了。
注意上图代表的仅是 LCD 显示器的物理像素情况,早期的 CRT 显示器的物理像素同样也是由独立的点组成。但是不存在 subpixel 的概念,情况如下图所示:
上面描述的这些显示器上的像素我们就称之为物理像素 (physical pixel) 或者设备像素 (device pixel)。
CSS 像素
作为 Web 开发者,我们接触的更多的是用于控制元素样式的样式单位像素。这里的像素我们称之为 CSS 像素。
CSS 像素有什么特别的地方?我们可以借用 quirksmode 中的这个例子:
假设我们用 PC 浏览器打开一个页面,浏览器此时的宽度为 800px,页面上同时有一个 400px 宽的块级元素容器。很明显此时块状容器应该占页面的一半。
但如果我们把页面放大(通过“Ctrl 键”加上“+ 号键”),放大为 200%,也就是原来的两倍。此时块状容器则横向占满了整个浏览器。
吊诡的是此时我们既没有调整浏览器窗口大小,也没有改变块状元素的 css 宽度,但是它看上去却变大了一倍——这是因为我们把 CSS 像素放大为了原来的两倍。
CSS 像素与屏幕像素 1:1 同样大小时:
CSS 像素 (黑色边框) 开始被拉伸,此时 1 个 CSS 像素大于 1 个屏幕像素
也就是说默认情况下一个 CSS 像素应该是等于一个物理像素的宽度的,但是浏览器的放大操作让一个 CSS 像素等于了两个设备像素宽度。在后面你会看到更复杂的情况,在高 PPI 的设备上,CSS 像素甚至在默认状态下就相当于多个物理像素的尺寸。
通过上面这个例子我想传递一个非常重要的概念,就是 CSS 像素从来都只是一个相对值。
正确答案
回到 PPI 上来,现在我们有了两种像素,设备像素和 CSS 像素。那么 PPI 中的像素是指哪一种?
请记住,PPI 中的 pixel 指的应该是物理像素。
但是在维基百科对 PPI 的解释中,pixel 被解释为一种类似于分辨率下的像素:
The apparent PPI of a monitor depends upon the screen resolution (that is, the number of pixels) and the size of the screen in use; a monitor in 800×600 mode has a lower PPI than does the same monitor in a 1024×768 or 1280×960 mode.
上面这段话是在说,同一尺寸的显示器在 800x600 分辨与 1024x768 分辨率下的像素密度明显是不同的,明显后者单位面积内的像素更多,当然后者的像素密度更高。
这里考虑了另一种情况,即在同一显示器下因为分辨率调整导致显示器的像素密度不同。这里的像素虽然是不是在浏览器中显示,但原理也类似于 CSS 像素,即由多个物理像素组成一个指定分辨率下的像素。
但问题是,这样的比较是没有任何意义的,我们通常在比较 PPI 时,一定是在跨设备比较,为了体现设备的技术优势,也一定是拿设备的最优或者极限情况进行比较,这样情况下分辨率下像素是与物理像素一一匹配的。 不能说一台 23 寸的 2k 显示器和一台 23 寸的 1080p 显示器因为都能调整到 1440x960 的分辨率,那么他们的 PPI 就相同了?PPI 终究是体现设备某方面性能的参数。
也就是说,当我们在谈论一台设备的 PPI 时,它是一个定值,是一个固定的参数。
那么 PPI 怎么计算呢?没错,就和你想的一模一样,用屏幕边的物理像素除以物理尺寸即可,以 Samsung Galaxy S4 为例:
由此可见 Galaxy S4 的屏幕分辨率为 441PPI。
The Bad and the Ugly
但 PPI 过高同样也会带来问题,相同的图片素材,在越高的设备上会显示的越小。以下是一个像素在不同 PPI 设备上的可见情况,随着 PPI 的增高可视度越来越小:
那么可以预见一种很糟糕的情况是,同一尺寸的屏幕下假设 PPI 提高了一倍,很可能程序界面缩小了 4 倍(因为在屏幕尺寸不变的情况下物理像素点面积是原来的 1/4)。
以 Surface Pro 3 为例,它的默认分辨率是 2160x1440,也就是说 Surface 这台设备的屏幕物理像素有 2160x1440 个点,同时默认分辨率情况下,一个点物理像素点对应于一个分辨率像素。 但因为屏幕只有 12 寸,像素密度非常高,于是就出现了上面的问题,各个文字和图标被缩的太小了,电脑是完全不可用的。
解决方法是,Windows 默认将所有的文本和素材(实际上就是分辨率像素)都放大了 1.5 倍(在“屏幕分辨率”-“放大或缩小文本和其他项”中进行了设置),原来是一个物理像素对应一个分辨率下的像素,现在则是 1.5 个物理像素对应一个分辨率下的像素,也就意味着分辨率下的像素变大了,实际分辨率降低了,已经变成了 1440(2160/1.5)x900(1440/1.5)(此时如果你尝试用 window.screen.width/window.screen.height 去检测返回结果也会是 1440x900)。这里留给读者一个问题,这样和直接将 PC 的分辨率调整为 1440x900 有什么区别呢?
但把素材和文字放大就真的一劳永逸了吗?不,甚至还会带来副作用。放大素材对位图来说是非常危险的一件事。假设一款软件中的素材图片分辨率为 32x32,但是为了配合整体界面的拉伸, 它也必须被拉伸至原来的 1.5 倍等于为 48x48。你一定有在 Photoshop 中把图片强制放大为原来几倍的效果的经验。 这样以来,图片素材就变得模糊了。同时因为 Window 使用的字体为点阵字体而非矢量字体,所以甚至在软件中的字体也会变得模糊。
简单一点来说,采用这种技术需要将 32x32 的图片强制拉伸为 48x48,多出来的像素如何凭空生成?计算机只有猜测了,通过线性插值算法。所以图片便会出现模糊。
但位图可能会被拉伸的问题并非也是绝对的,假设软件需要显示的 icon 大小为 32x32,但是图片素材大小为 64x64,那么即使 Windows 的 UI 界面拉伸 1.5 倍,icon 大小为 48x48,因为原图片足够大,图片仍处于未拉伸的状态。那么也不会模糊。
反过来我们可以得出结论,为了让在低 PPI 上和高 PPI 上图片显示的效果一致,图片素材应该尽可能的高清。
Apple 的 Retina 技术使用的也是上面相同的方案。以 15.4 寸的 Retina 版 Macbook Pro 为例。显示屏的物理像素点实际上有 2880x1880,但其实默认的最优分辨率只有 1440x900,刚好是物理像素的一半。也就是说操作系统默认使用了 4:1 的缩放。但这同样也有可能会出现使用软件虚化的问题。
我不清楚 Mac 软件开发中是如何解决这个问题的,但可以参考 iPhone 开发中的解决方案,苹果鼓励开发者准备两份素材,普通和高清素材。并且通过素材文件名后缀来区分,比如普通素材名称为 apple.png,那么高清素材名称就为apple@2x.png 。自然高清素材是普通素材面积的四倍,系统会优先使用高清素材,但自动缩小到普通素材的大小,这样也就不存在图片拉伸的问题了。
PPI 之于 Web
从上面我们得知,因为高像素密度设备下的 UI 会采用一定比例的缩放,所以 CSS 像素也会面临同样的问题:
正如上图所示,左侧普通屏幕中,2x2 的 CSS 像素真的只需要 2x2 的物理像素。但是右侧高清屏中,2x2 的 CSS 像素却需要 4x4 的物理像素。
我刚刚有说道解决高清 PPI 下图片渲染问题的方法之一就是使用更高清的图片素材。但问题是需要有多高清?
在 Retina 显示屏上,根据上一节描述的原理,当我们需要渲染一张 32x32 的图片,我们实际上需要准备 64x64 的素材。因为苹果默认把所有素材都进行了两倍的放大。但如果有一台更高清的设备,进行了三倍或者四倍或者更高的倍数,我们岂不是需要准备更多尺寸或者体积更大的文件素材?在 Web 开发中我们正在面临这样的问题。
首先我们要学会如何表达和判断这样一种 CSS 像素和物理像素不平等。
DevicePixelRatio
DevicePixelRatio 定义如下:
window.devicePixelRatio = physical pixels / dips
分母 dips 全称为 device-independent pixels,译为 _ 与设备无关像素 _。 更通俗的说应为与物理像素无关的 CSS 像素。
以 iPhone4 为例,在垂直状态下手机的物理像素宽度有 640px,但是因为 2:1 缩放的关系,此时的 dip,设备报告给我们的宽度只是 320px。 此时的 DevicePixelRatio 就为 640 / 320 = 2;
devicePixelRatio 说白了就是手机的物理像素与实际使用像素的缩放比。
注意 devicePixelRatio 并非是一个默认值。在默认情况下 CSS 像素是由手机默认的缩放决定的。但同时因为浏览器页面也可以被人为的进行缩放。比如 iPhon4 中默认的分辨率宽度为 320px。浏览网页时我们完全可以自行放大两倍为 160px。这样以来 window.devicePixelRatio 就变味了 640 / 160 = 4。
dppx
与 divicePixelRatio 几乎等价的一个概念时 dppx:dots per pixel。 表示单个 CSS 像素占用的物理像素个数。仔细想想,这与 devicePixelRatio 其实是一个意思, iPhone4 的 dppx 为 2,不就是与 devicePixelRatio 刚好相等吗。devicePixelRatio 是从宏观上来说这件事。把整体宽度做运算。dppx 是从微观角度上说这件事,考虑的是单个像素之间的比较。
dpi
请记住,当我们在谈论一台显示设备的像素密度时,dpi 与 ppi 是等价的。dots per pixel 中的 dots 就是代指物理像素。
但是如果你在 mediaquery 中使用 dpi 是就要注意了,Chrome 会在控制台中提示你使用 dppx 而非 dpi:
Consider using ‘dppx’ units instead of ‘dpi’, as in CSS ‘dpi’ means dots-per-CSS-inch, not dots-per-physical-inch, so does not correspond to the actual ‘dpi’ of a screen. In media query expression: only screen and (-webkit-min-device-pixel-ratio: 2), not all, not all, only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx)
上面这段话的意思是,在 mediaquery 中 inch 表示的 CSS 定义中的一英寸,而非生活中物理定义的一英寸。
实话实说我并没有找到关于 CSS 中一英寸的定义,但是在 W3C 关于 Resolution 的定义中,我们可以看到看到它所定义的 1dppx 是与 96dpi 具有同样含义的。那么 2dppx 也就是 192dpi 了咯。这当然脱离了我们传统上的 dpi 了,Surface Pro 3 的 dpi(也就是 ppi)能够达到 216ppi,但是在默认未放大界面时的 dppx 仍然可以是 1。
CSS Reference Pixel
个人人为这是一个很鸡肋的概念,但也正是因为了解的人太少了,还是需要值得一提。
假设我们规定了 CSS 像素值需要与设备像素大小相等,但当随着手持设备距离人的远近不同,设备像素密度的不同,都会导致我们看见的设备上的 CSS 像素的可见大小发生变化(类似于巨大的月亮因为离地球遥远在人眼看来也不过像硬币一样大小)。为了保证 CSS 像素在不同设备和不同距离上观测到的大小保持一致保持连贯性。W3C 定义了一个 CSS 相对像素(CSS reference pixel)的概念
It is recommended that the reference pixel be the visual angle of one pixel on a device with a pixel density of 96dpi and a distance from the reader of an arm’s length. For a nominal arm’s length of 28 inches, the visual angle is therefore about 0.0213 degrees.
W3C 规定,把人眼能够辨别到的,距离自己一个手臂长度(约 28 英寸),像素密度为 96dpi 设备上的一个物理像素设为参考像素。所以我们可以算出眼睛看到参考像素的视野角度为 0.0213 度:
有了这一系列参照,通过三角函数关系,我们可以算出同样一台设备在不同距离下 CSS 像素理想的大小。 当远离观察者时像素应该增大,当靠近观察者时像素应该减小:
这么做的优势在于无论设备距离观察者距离是多少,也无论设备的像素密度和物理像素大小是多少,观察者看到的 CSS 像素是一致的,保证了用户体验的一致性:
但问题是如何来实践这一标准呢?
<meta name="viewport">
我们有了物理像素,CSS 像素——那么问题来了,当你再手机上使用浏览器打开网页时,网页应该按照哪一种宽度进行渲染?
首先我们需要了解一个概念:viewport,我常见到的中文译为视口,但个人觉得这个翻译有一些晦涩。 Viewport 是用于限制 Html 元素——“限制”这两个字不是那么好理解。quirksmode 上有一篇文章谈到这个概念时打了一个非常形象的比方:
假设body 标签内有一个块状元素宽度为10%: div {width:10%;}
,我们知道当我们缩放浏览器时这个块状元素的宽度也会跟着变化。 这是因为它的宽度占它父元素的 10%。那么它的父元素,也就是 body 元素的宽度是由谁决定的呢?
我们知道一个块状元素默认宽度为它父元素的 100%,也就是 body 元素的宽度与包裹它的 html 元素宽度相同。那么问题又变成了 html 元素的宽度是由谁决定的?
答案是浏览器窗口。现在我们可以归纳起来,html 元素是被浏览器限制并且包裹起来的。html 的宽度就是浏览器的宽度。
但事实上,html 元素宽度是占据 viewport 的 100%,而在桌面浏览器中,viewport 与浏览器窗口大小刚好相等(注意,这仅仅是在桌面浏览器上)。
OK,在于是我们得到了一个结论,html 宽度是由 viewport 决定的,但是 在桌面浏览器中,viewport 大小与浏览器窗口大小相等。
但这一套规则在手机则是无法被执行的。大部分手机的屏幕分辨率目测只有 400px,如果页面上真的有某一个页面元素仅占 10%,也就是 40px 的话,肉眼几乎是无法分辨的。实际情况应该会更糟糕,iPhone4 的 Safari 默认是以 980px 来渲染网页的。如果你在 Chrome 以桌面版的方式访问 stackoverflow,那么结果会是这样的:
体验非常糟糕吧,所有的链接几乎都无法准确点击。那么如何解决这个问题?
第一个办法,放大页面。
我们会很习惯的用手势去放大页面。但是要注意我们这里做的仅仅是放大页面,改变的是页面的缩放 (scale),效果与 PC 上浏览器的类似。但是没有改变页面的布局,此时用于渲染页面布局的 layout 仍然是 980px
第二个办法是,改变布局。
比如下面一个页面上有一张 320px 宽的图片,如果我们以默认的 980px 去渲染的话,它会显得过于窄小:
但如果我们可以将渲染它的布局设为 320px 的话,看上去就会好很多了,同时此时我们也未对页面进行缩放:
当然你也可以结合上一步,同时对页面进行缩放:
不仅仅是放大,即使是在 320px 的像素下,我们也可以进行缩小:
回归到技术上,以上这些都可以通过 viewport 标签来解决,比如说上面的需求,把布局设定为 320px,同时进行 1.5 倍的缩放:
<meta name="viewport" content="width=320, initial-scale=1.5">
所见即所得,需要设置的属性在 content 以逗号分割开来,width
表示页面布局宽度,initial-scale
代表页面初始状态的缩放比例,如果你不想让用户进行缩放,还可以添加user-scalable=no
字段来保证用户无法进行缩放。
更重要的是,我们还可以无需指定特定宽度,通过设置width=device-width
,指定布局宽度等于手机分辨率宽度(但是我们不用关心手机分辨宽度是什么)来更好的利用响应式设计。注意这里的device-width
表示手机的分辨率宽度,而并非手机物理像素宽度。iPhone4 在垂直状态下物理像素宽度为 640,这里的device-width
代表的则应该是它的 dip 像素 320px。
给 viewport 标签添加width=device-width
适用于这样一种情况:你在为移动设备开发的响应式网页时,你会面临多重分辨率情况,但是你又没有必要使用到重量级的 mediaquery,同时也为了避免手机浏览器使用桌面分辨率宽度去渲染页面, 同时这还能兼容在手机横握或者竖握的情况。 这样让你的响应式页面能够适用大多数的移动设备。
写到这里我们可以做一个总结,viewport 标签的作用是什么?它能够让你撇开设备的干扰,告诉设备你想用什么样的宽度渲染网页。让它听命于你,而不是你听命于他。
上面我们谈到 viewport 有个半专业的名词成为 layout viewport,虽然它是一个非官方的词汇,但是非常多的文章都引用了这个概念。layout viewport 专用于页面渲染的控制。还有一种 viewport 称之为 visual viewport,可以译为可视窗口。两种 viewport 的区分如下:
由此可以看出 visual viewport 就好比是浏览网页的一个窗口,网页正是这窗外的景色。当然我们还会遇见 layout viewport 与 visual viewport 大小相等的情况。比如像下面这样:
这也就是我上面描述的width=device-width
了。
番外篇:PPI 和 DPI 使用的更多场景
在文章的开头我有说 PPI 在不同上下文中的含义是不同的,如果你仍有好奇心,可以继续往下阅读。接下来我们谈谈 Web 以外的 PPI 含义。
首先我们要重申上面的结论,就谈论显示设备的像素密度而已,PPI 和 DPI 和一样的概念,并且其中的像素 pixel 和点 dots 代指的都是物理像素。
如果你去查看一张 JPG 图片的属性时,你会发现有横向或者纵向的以 dpi 为单位的属性或者在 Phototshop 新建一份文档时,要填写一个以 ppi 为单位的属性值:
这里也存在被混用和混淆的地方。其实他们都表示打印时的分辨率值。意为在打印时每英寸上的像素(也就是跟接近 PPI,但我们更常用 DPI)。这里的英寸当然不再是屏幕像素了,而是纸张尺寸了。
PPI 或者 DPI 对于图片来说意味着什么?准确来说什么都不意味着。 一张图片只是存在相机或者硬盘里的数据文件而已,你能告诉我它有多少英寸长或者多少英寸宽吗?只有当它被打印出来的时候才会涉及到打印媒介的尺寸,DPI 才有意义。 如果你想让图片更丰富,唯一的办法是增加图片的像素,提升你的拍摄技巧。
当然在纸张上是没有像素的概念。但我们可以去抽象的去想象它。假设有一张 300x300 像素的图片。打印分辨率的为 30DPI, 那么最后打印出来尺寸为 10x10 英寸。假如打印时的 DPI 值为 300DPI,那么打印出来的尺寸则为 1x1 英寸。所以我们可以把 DPI 当做调节打印尺寸大小的手段。
那么 DPI 值越高,图片就越小就越清晰?当然也并非如此。如果你距离 60 厘米去观看一张 194DPI 打印出来的图片。你会没法区分它到底是 194DPI 还是 300DPI。因为人眼的分辨率是有限的。这对显示设备同样通用的。iPhon4 的像素密度有 326DPI,而 New iPad 的像素密度只有 264DPI,New iPad 的显示效果会更差吗?参考大多数人使用的距离和方式,其实眼睛得到的效果其实是无太大差异的。这也是为什么大型显示器或者户外广告 DPI 都不会很高,因为我们观看他们的时候距离很远,效果并非太差。
最后我们可以来看另一个场景的 DPI:描述打印机的打印分辨率:
当一张显示器上的图片打印在图片上的时候,像素这个概念其实是我们想象出来的,更加实际的概念时是印刷设备的每一个“点”:
当你尝试去用放大镜去查看彩色印刷物品上的图片时,从小到大你看到的结果应该是这样的:
为什么会这样?简而言之,印刷的原理是通过半色调 (halftone) 技术,通过控制 CMYK 四种颜色点印刷时的每一个印刷点的大小,角度,间隙来模拟出一种颜色的感觉:
比如当你以 600DPI 打印一张 150PPI 的图片时,每一个像素应该包含 16 个点 (600dots / 150pixels = 4)。
从上面我们已经知道 PPI 能够决定印刷品物理尺寸的大小,打印机的 DPI 参数更是能进一步决定印刷体的好坏。我们用于都在追求更高的 DPI 和 PPI。
150dpi 通常已经是被认为算的上是高质量的打印分辨率了。新闻报纸使用的分辨率通常是 85dpi。户外的广告牌通常使用的是 45dpi。但是因为距离的关系你不会觉得他们的印刷质量太差。
结尾
这篇文章我把移动开发中可能会涉及到的概念都做了一些涉及。在下篇中我将运用到这些概念,并且总结在移动设备上的图片加载方案。
参考文献(已在文中引用的就不在此列举了):
- devicePixelRatio
- Pixels per inch is just a tag
- Configuring the Viewport
- PPI vs. DPI: what’s the difference?
- The Myth of DPI
- DPI Myth
- What is DPI?
- Pixel Density: Pixels Per Inch (PPI) Explained
感谢王保平对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。
评论