移动端设备上,不论是浏览器适配还是Android开发,最常见的需求是:不同设备不同分辨率上,同一份设计稿等比例适配的问题;另外,针对于大屏幕设备(如:pad),如何展现更多内容,呈现非等比例适配,也是一类需求;
本文的介绍对于浏览器开发和Android开发帮助都不大,做移动端适配的前端开发者请参考其它资料;作者旨在完成调研:通过示例来证明背后的关联关系;
以往其它作者的相关文章,一上来先介绍CSS像素,不太习惯;其实最好从硬件底层开始,自底向上介绍会理解更深刻;本文介绍先从DPI开始;
1. DPI(Dot Per Inch)每英寸点数/像素密度
这个词在打印行业中用的较多,代表:画布上单位面积内的物理实体的色块点数;
在打印中用于标识打印机的精度高低,用作打印机的分辨率单位;DPI值越高,表明打印机的打印精度越高。
例如:HP喷墨打印机最高标称分辨率是4800×1200dpi,这意味着在纸张的X方向(横向)上,每一英寸长度上理论上可以放置4800个墨点。
在Android设备中,DPI既可以是一个物理实体概念(代表设备生产时每英寸有多少个色块点),也可以是一个虚拟逻辑概念(OS层面虚拟创建的每英寸色块点数);虚拟层的数值与物理层的数组可以不相同,这种非一一映射的情况可以通过OS底层驱动进行适配即可;
大家可以通过Android中的类DisplayInfo.java来理解,如下面代码所示:
/**
* The logical display density which is the basis for density-independent
* pixels.
*/
public int logicalDensityDpi;
/**
* The exact physical pixels per inch of the screen in the X dimension.
* <p>
* The value of this field is indeterminate if the logical display is presented on
* more than one physical display.
* </p>
*/
public float physicalXDpi;
/**
* The exact physical pixels per inch of the screen in the Y dimension.
* <p>
* The value of this field is indeterminate if the logical display is presented on
* more than one physical display.
* </p>
*/
public float physicalYDpi;
上图中Android OS会存储三个值:物理层的X方向DPI,物理层的Y方向DPI,以及逻辑层的像素密度相关DPI(下面会介绍);
开发者可以通过ADB命令查看这几个值,同时也可以使用ADB对变量logicalDensityDpi进行修改,以达到修改分辨率与密度无关像素DP的目的;
初期的移动设备上,常见为160dpi,即:每英寸拥有160个色块点;而PC端设备上,DPI范围在72~120之间;
参考:
Android官方:像素密度
百度百科:DPI
2. DIP/DP(Device Independent Pixel / Density Independent)设备独立像素/密度无关像素
同样的屏幕尺寸,拥有高低不同的DPI;如果软件设计中使用DPI作为单位,如果要保持一个图片在不同设备显示同样大小,那么设计稿需要针对不同的DPI提供不同的占比值;
比如:设计稿如果按照1px比1dpi的比例,在160DPI设备的设计稿为160px;在300DPI设备的设计稿为300DPI;
这样不好在于:
1) 提供多套设计稿;
2) 软件设计与物理设备映射紧密,如果出了新设备再适配就更麻烦;
所以出现新的方案:Android 1.8版本中增加的逻辑像素单位DP;
例如:原有mdpi设备为160dpi,倍率定义为1,即:1px = 1dp;那么320x480px对应320x480dp;
例如:针对hdpi设备为240dpi,倍率定义为1.5,即:2px=1dp;那么480x800px对应320x533.33dp;
例如:针对xhdpi设备为320dpi,倍率定义为2,即:2px=1dp;那么640x960px对应320x640dp;
例如:针对xxhdpi设备为480dpi,倍率定义为3,即:3px=1dp;那么1080x1920px对应360x640dp;
例如:针对ldpi设备为120dpi,倍率定义0.75;,即:0.75px=1dp;那么240x320px对应320x426.66dp;
所以,Android开发中UI提供的设计稿,需要提供:mdpi * 最高倍率(一般为3) 的素材,才能完成向下的轻松适配;
上面的倍率称为:密度换算因子(Desity Conversion Factor),代表:该设备类型的DPI与基准DPI的比;
在Android开发中常见使用DP作为单位,如果以360*640dp作为绘图尺寸,那么针对市场上不同DP宽度的设备,开发时就需要定义不同的布局声明;如:一个TextView组件在不同设备拥有不同的DP值;
参考:
Android官方:密度无关像素
Vinsol:开发者眼中的设计
Youtube(DIP介绍)
- 最小宽度:
在部分手机的Android开发者选项中有一个"最小宽度"的设置,用于设置屏幕宽度的值,单位为DP;
当修改设置中的"显示大小"时(用于适配老人等不同人群),这个DP值也跟着发生变化;
由此来看,DP值、屏幕分辨率、DPI值存在一定的换算关系:
最小宽度DP值 = 分辨率的宽px x 160 / DPI值;
参考:
DP与分辨率换算
- ADB修改DPI与分辨率
针对Android设备,通过ADB命令可以查看/修改屏幕的分辨率,DPI;
命令:查看系统的分辨率与DPI:adb shell dumpsys window displays
命令:修改分辨率:adb shell wm size 1520x720
命令:修改DPI: adb shell wm density 300
命令:恢复设置: wm xxx reset
以作者的小米8青春版手机为例,手机设置中显示:
分辨率为:2280x1080像素,最小宽度为:392dp;
通过ADB命令查看,如下:
mDisplayInfo=DisplayInfo{"内置屏幕", uniqueId "local:0", app 1080 x 2068, real 1080 x 2280, largest app 2198 x 2068, smallest app 1080 x 998, mode 1, defaultMode 1, modes [{id=1, width=1080, height=2280, fps=60.000004}], colorMode 7, supportedColorModes [7], hdrCapabilities android.view.Display$HdrCapabilities@40f16308, rotation 0, density 440 (403.411 x 402.166) dpi, layerStack 0, appVsyncOff 1000000, presDeadline 16666666, type BUILT_IN, state ON, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, removeMode 0}
复制其中的文字可以看到:density 440 (403.411 x 402.166) dpi;
即:虚拟DPI为440,实际的物理DPI在XY分别为括号中的值;
根据提示,此处的信息展现也是来自于本文最开始介绍的类DisplayInfo与类Display。
通过上面的公式也可以证明:DP与分辨率之间的换算关系;
392dp = 1080px * 160 / 440dpi;
当开发者通过ADB命令更新分辨率或者DPI时,会发现手机设置中的DP值也跟着发生改变,与公式计算一致;
3. PPI(Pixels Per Inch)每英寸像素数
可以用于衡量:观看移动设备时是否产生颗粒感;
乔布斯在iPhone 4发布会上对视网膜技术做介绍,分辨率达到300ppi以上,视网膜就无法分辨出像素颗粒点了;
与分辨率相关,计算方式:PPI = √(X^2 + Y^2)/ Z
(X:长度像素数;Y:宽度像素数;Z:屏幕英寸大小(对角线英寸长度))
例如:iPhone4的分辨率是960×640像素。 iPhone4采用3.5英寸IPS屏幕,同时加入了新的Retina Display(视网膜)显示技术,每英寸的面积里有326个像素;
PPI值 = √(960^2 + 640^2)/ 3.5 = 326PPI;通常电脑显示屏幕的分辨率为72ppi,iPhone4的分辨率为电脑的4倍多,所以显示会非常细腻。
上面这块源于网上内容,个人认为:如果没有设备高精度的DPI支持,那么此处的PPI是无意义的;
4. W3C中的viewport信息
在移动端浏览器中,最初为了在窄屏幕的设备上显示面向PC的网页内容,增加了viewport的meta信息;
如果viewport无声明,则默认屏幕width为980px,即屏幕可以滚动;
如果设置为device-width代表屏幕全宽,等同于screen.width;
<meta name="viewport" content="width=device-width">
使用作者的手机浏览器测试后,发现:
screen.width等于开发者选项中的最小宽度,即:dp值;
相应的devicePixelRatio为 屏幕分辨率的宽度 / dp值;
参考:
MDN 980虚拟视口
PPK视口转述
然而实际上,在Android设备上Webview呈现并不需要占据屏幕全宽;
那么当WebView在设备宽度为420dp的其中300dp上展现时(非全宽情况),此时的device-width会是多少呢?
经过测试,得到的结论是:
device-width代表视觉视口的宽度DP值;如果想要屏幕宽度DP值,那么通过screen.width获取;
以下面的代码为例,读者可以针对不同的meta声明,查看不同单位的展示效果;
<!doctype html>
<html>
<head>
<title>首页</title>
<meta name="viewport" content="width=device-width">
<!--<meta name="viewport" content="initial-scale=1.0">-->
<!--<meta name="viewport" content="width=980,initial-scale=1.0">-->
<!--<meta name="viewport" content="initial-scale=2,user-scalable=no">-->
<!--<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no">-->
<style>
body {
margin: 0;
}
.bg-gray {
background-color: #d3d3d3;
}
</style>
</head>
<body>
<!--<div class="bg-gray" style="width: 980px;">980px</div>-->
<!--<div class="bg-gray" style="width: 490px;">490px</div>-->
<div class="bg-gray" style="width: 300px;">300px</div>
<div class="bg-gray" style="width: 50%;">50%</div>
<div class="bg-gray" style="width: 100vw;">100vw</div>
<div></div>
<p id="num1"></p>
<script>
console.info(`innerWidth:`, window.innerWidth, window.outerWidth, window.devicePixelRatio, screen.width, screen)
var text1 = [
`document.clientWidth=${ document.documentElement.clientWidth}`,
`window.innerWidth=${window.innerWidth}`,
`window.outerWidth=${window.outerWidth}`,
`window.devicePixelRatio=${window.devicePixelRatio}`,
`screen.width=${screen.width}`
]
document.getElementById('num1').innerHTML = text1.join('<br>')
</script>
</body>
</html>
同时,针对Android设备中,不同的Webview宽度(下图中上面为200dp,下面为300dp),来查看实际效果;
想要获取这个示例的读者,可以下载该APK示例项目进行测试与确认;
链接: https://pan.baidu.com/s/11CoYE2TuvTiqxmnGzUz89A 提取码: 3bs1
5. DPR 浏览器中的devicePixelRatio 设备像素比
MDN定义为:CSS像素与物理像素的比率;
根据上面推算,分辨率中的宽度像素值就是CSS像素值;物理像素值指的是DP值;
即:DPR = 分辨率中的宽度值 / 最小宽度DP值
以作者手机为例:2.75dpr = 1080px / 393dp;
当通过ADB命令修改分辨率与DPI后,相应DPR也会跟着变化,此处不再赘述;
参考:
Github中某作者的理解
第三代移动端布局方案