像素和dpr
定义
像素,又称画素,是图像显示的基本单位,译自英文“pixel”,pix 是英语单词 picture 的常用简写,加上英语单词“元素” element,就得到 pixel,故“像素”表示“图像元素”之意,有时亦被称为 pel(picture element)
像素是网页布局的基础。一个像素就是计算机能够显示一种特定颜色的最小区域。当设备尺寸相同但像素变得更密集时,屏幕能显示的画面的过渡更细致,网站看起来更明快。
ppi 是指屏幕上每英寸可以显示的像素点的数量,即屏幕像素密度
分类
像素分为两种:设备像素和 CSS 像素
- 设备像素(device independent pixels): 设备屏幕的物理像素,任何设备的物理像素的数量都是固定的
- CSS 像素(CSS pixels): 又称为逻辑像素,是为web开发者创造的,在 CSS 和 javascript 中使用的一个抽象的层
CSS 声明和几乎所有的 javascript 属性都使用 CSS 像素,因此实际上从来用不上设备像素,唯一的例外是screen.width/height
//我们通过CSS和javascript代码设置的像素都是逻辑像素
width:300px;
font-size:16px;
缩放
(设备像素→深蓝色背景,CSS 像素→半透明背景)
在桌面端,css 的1个像素往往都是对应着电脑屏幕的1个物理像素。
//一个 CSS 像素完全覆盖了一个设备像素
而在手机端,由于屏幕尺寸的限制,缩放是经常性的操作。
//左图表示当用户进行缩小操作时,一个设备像素覆盖了多个 CSS 像素
//右图表示当用户进行放大操作时,一个 CSS 像素覆盖了多个设备像素
不论我们进行缩小或放大操作,元素设置的 CSS 像素(如width:300px
)是始终不变的,而一个 CSS 像素对应多少个设备像素是根据当前的缩放比例来决定的
DPR
设备像素比 DPR (devicePixelRatio) 是默认缩放为100%的情况下,设备像素和 CSS 像素的比值
DPR = 设备像素 / CSS像素(某一方向上)
在早先的移动设备中,并没有 DPR 的概念。随着技术的发展,移动设备的屏幕像素密度越来越高。从 iphone4 开始,苹果公司推出了所谓的 retina 视网膜屏幕。之所以叫做视网膜屏幕,是因为屏幕的 PPI (屏幕像素密度)太高,人的视网膜无法分辨出屏幕上的像素点。iphone4 的分辨率提高了一倍,但屏幕尺寸却没有变化,这意味着同样大小的屏幕上,像素多了一倍,于是 DPR = 2
CSS 像素对应着理想视口,其对应的javascript属性是screen.width/screen.height
。设备像素比 DPR 对应的 javascript 属性是window.devicePixelRatio
以 iphone5 为例,iphone5 的 CSS 像素为320px * 568px
,DPR 是 2,所以其设备像素为640px * 1136px
640(px) / 320(px) = 2
1136(px) / 568(px) = 2
640(px)*1136(px) / 320(px)*568(px) = 4
常用移动设备dpr
IOS设备 | PPI | DPR |
---|---|---|
iPhone4 | 326 | 2 |
iPhone4s | 326 | 2 |
iPhone5 | 326 | 2 |
iPhone5s | 326 | 2 |
iPhone6 | 326 | 2 |
iPhone6s | 326 | 2 |
iPhone6 PLUS | 441 | 3 |
iPhone6s PLUS | 441 | 3 |
iPad - | - | 2 |
iPad - | - | 3 |
android | 尺寸 | dpr |
---|---|---|
低清设备 | - | 1 |
- | - | 1.5 |
- | - | 1.75 |
- | - | 2 |
mx2 | 800*1280 | 2.5 |
小米note | 720*1280 | 2.75 |
- | - | 3 |
三星note4 | - | 4 |
... | ... | ... |
... | ... | ... |
总的来说,ios设备的dpr总体来说比较一致。
解决方案
1. 淘宝的移动端布局方案 Flexible
/
hotcss 可能有神奇的问题
/
!function() {
var innerStyle = "@charset \"utf-8\";html{color:#000;background:#fff;overflow-y:scroll;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}html {outline:0;-webkit-text-size-adjust:none;-webkit-tap-highlight-color:rgba(0,0,0,0)}html,body{font-family:sans-serif}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td,hr,button,article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{margin:0;padding:0}input,select,textarea{font-size:100%}table{border-collapse:collapse;border-spacing:0}fieldset,img{border:0}abbr,acronym{border:0;font-variant:normal}del{text-decoration:line-through}address,caption,cite,code,dfn,em,th,var{font-style:normal;font-weight:500}ol,ul{list-style:none}caption,th{text-align:left}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:500}q:before,q:after{content:''}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}a:hover{text-decoration:underline}ins,a{text-decoration:none}",
createStyle = document.createElement("style");
if (document.getElementsByTagName("head")[0].appendChild(createStyle),
createStyle.styleSheet)
createStyle.styleSheet.disabled || (createStyle.styleSheet.cssText = innerStyle);
else
try {
createStyle.innerHTML = innerStyle;
} catch (ex) {
createStyle.innerText = innerStyle;
}
}();
! function(window, nameSpace) {
var timer, doc = window.document,
docEl = doc.documentElement,
metaEl = doc.querySelector('meta[name="viewport"]'),
flexibleEl = doc.querySelector('meta[name="flexible"]'),
dpr = 0,
scale = 0,
Flexible = nameSpace.flexible || (nameSpace.flexible = {});
// 给Flexible 开创命名空间
//刷新rem
function refreshRem() {
var width = docEl.getBoundingClientRect().width;
//width / dpr > 540 && (width = 540 dpr);
if(width / dpr > 540) {
width = 540 dpr ;
}
var rootSize = width / 10;
docEl.style.fontSize = rootSize + "px",
Flexible.rem = window.rem = rootSize;
}
if (metaEl) {
console.warn('将根据已有的meta标签来设置缩放比例');
var match = metaEl.getAttribute('content').match(/initial-scale=([\d.]+)/);
if (match) {
scale = parseFloat(match[1]);
dpr = parseInt(1 / scale);
}
//如果在meta标签中,我们手动配置了flexible,则使用里面的内容
} else if (flexibleEl) {
var content = flexibleEl.getAttribute('content');
if (content) {
var initialDpr = content.match(/initial-dpr=([\d.]+)/);
var maximumDpr = content.match(/maximum-dpr=([\d.]+)/);
if (initialDpr) {
dpr = parseFloat(initialDpr[1]);
scale = parseFloat((1 / dpr).toFixed(2));
}
if (maximumDpr) {
dpr = parseFloat(maximumDpr[1]);
scale = parseFloat((1 / dpr).toFixed(2));
}
}
}
if (!dpr && !scale) {
var isAndroid = window.navigator.appVersion.match(/android/gi);
var isIPhone = window.navigator.appVersion.match(/iphone/gi);
//devicePixelRatio这个属性是可以获取到设备的dpr的
var devicePixelRatio = window.devicePixelRatio;
if (isIPhone) {
if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
dpr = 3;
} else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
dpr = 2;
} else {
dpr = 1;
}
} else {
dpr = 1;
}
scale = 1 / dpr;
}
if (docEl.setAttribute("data-dpr", dpr), !metaEl)
if (metaEl = doc.createElement("meta"),
metaEl.setAttribute("name", "viewport"), //j = scale //j = scale //j = scale
metaEl.setAttribute("content", "initial-scale=" + scale + ", maximum-scale=" + scale + ", minimum-scale=" + scale + ", user-scalable=no"),
docEl.firstElementChild)
docEl.firstElementChild.appendChild(metaEl);
else {
var createDiv = doc.createElement("div");
createDiv.appendChild(metaEl),
doc.write(createDiv.innerHTML)
}
window.addEventListener("resize", function() {
clearTimeout(timer),
timer = setTimeout(refreshRem, 300);
}, !1),
window.addEventListener("pageshow", function(a) {
a.persisted && (clearTimeout(timer),
timer = setTimeout(refreshRem, 300))
}, !1),
"complete" === doc.readyState ? doc.body.style.fontSize = 17 dpr + "px" : doc.addEventListener("DOMContentLoaded", function() {
doc.body.style.fontSize = 17 dpr + "px"
}, !1),
refreshRem(),
Flexible.dpr = window.dpr = dpr,
Flexible.refreshRem = refreshRem,
Flexible.rem2px = function(a) {
var pxValue = parseFloat(a) this.rem;
return "string" == typeof a && a.match(/rem$/) && (pxValue += "px"), pxValue ;
},
Flexible.px2rem = function(a) {
var remValue = parseFloat(a) / this.rem;
return "string" == typeof a && a.match(/px$/) && (remValue += "rem"), remValue ;
}
}(window, window.lib || (window.lib = {}));
核心方法如下:
if (metaEl) {
console.warn('将根据已有的meta标签来设置缩放比例');
var match = metaEl.getAttribute('content').match(/initial-scale=([\d.]+)/);
if (match) {
scale = parseFloat(match[1]);
dpr = parseInt(1 / scale);
}
//如果在meta标签中,我们手动配置了flexible,则使用里面的内容
} else if (flexibleEl) {
var content = flexibleEl.getAttribute('content');
if (content) {
var initialDpr = content.match(/initial-dpr=([\d.]+)/);
var maximumDpr = content.match(/maximum-dpr=([\d.]+)/);
if (initialDpr) {
dpr = parseFloat(initialDpr[1]);
scale = parseFloat((1 / dpr).toFixed(2));
}
if (maximumDpr) {
dpr = parseFloat(maximumDpr[1]);
scale = parseFloat((1 / dpr).toFixed(2));
}
}
}
if (!dpr && !scale) {
var isAndroid = window.navigator.appVersion.match(/android/gi);
var isIPhone = window.navigator.appVersion.match(/iphone/gi);
//devicePixelRatio这个属性是可以获取到设备的dpr的
var devicePixelRatio = window.devicePixelRatio;
if (isIPhone) {
if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
dpr = 3;
} else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
dpr = 2;
} else {
dpr = 1;
}
} else {
dpr = 1;
// 其他设备仍旧使用1倍的方案
}
scale = 1 / dpr;
}
这里只对ios作了处理,android 仍采用了1倍的布局方案。
2. 另一种方案 hotcss.js
(function(window, document) {
'use strict';
//给hotcss开辟个命名空间,别问我为什么,我要给你准备你会用到的方法,免得用到的时候还要自己写。
var hotcss = {};
(function() {
//根据devicePixelRatio自定计算scale
//可以有效解决移动端1px这个世纪难题。
var viewportEl = document.querySelector('meta[name="viewport"]'),
hotcssEl = document.querySelector('meta[name="hotcss"]'),
dpr = window.devicePixelRatio || 1,
maxWidth = 540,
designWidth = 0;
dpr = dpr >= 3 ? 3 : (dpr >= 2 ? 2 : 1);
//允许通过自定义name为hotcss的meta头,通过initial-dpr来强制定义页面缩放
if (hotcssEl) {
var hotcssCon = hotcssEl.getAttribute('content');
if (hotcssCon) {
var initialDprMatch = hotcssCon.match(/initial-dpr=([\d.]+)/);
if (initialDprMatch) {
dpr = parseFloat(initialDprMatch[1]);
}
var maxWidthMatch = hotcssCon.match(/max-width=([\d.]+)/);
if (maxWidthMatch) {
maxWidth = parseFloat(maxWidthMatch[1]);
}
var designWidthMatch = hotcssCon.match(/design-width=([\d.]+)/);
if (designWidthMatch) {
designWidth = parseFloat(designWidthMatch[1]);
}
}
}
document.documentElement.setAttribute('data-dpr', dpr);
hotcss.dpr = dpr;
document.documentElement.setAttribute('max-width', maxWidth);
hotcss.maxWidth = maxWidth;
if (designWidth) {
document.documentElement.setAttribute('design-width', designWidth);
hotcss.designWidth = designWidth;
}
var scale = 1 / dpr,
content = 'width=device-width, initial-scale=' + scale + ', minimum-scale=' + scale + ', maximum-scale=' + scale + ', user-scalable=no';
if (viewportEl) {
viewportEl.setAttribute('content', content);
} else {
viewportEl = document.createElement('meta');
viewportEl.setAttribute('name', 'viewport');
viewportEl.setAttribute('content', content);
document.head.appendChild(viewportEl);
}
})();
hotcss.px2rem = function(px, designWidth) {
//预判你将会在JS中用到尺寸,特提供一个方法助你在JS中将px转为rem。就是这么贴心。
if (!designWidth) {
//如果你在JS中大量用到此方法,建议直接定义 hotcss.designWidth 来定义设计图尺寸;
//否则可以在第二个参数告诉我你的设计图是多大。
designWidth = parseInt(hotcss.designWidth, 10);
}
return parseInt(px, 10) 320 / designWidth / 20;
}
hotcss.rem2px = function(rem, designWidth) {
//新增一个rem2px的方法。用法和px2rem一致。
if (!designWidth) {
designWidth = parseInt(hotcss.designWidth, 10);
}
//rem可能为小数,这里不再做处理了
return rem 20 designWidth / 320;
}
hotcss.mresize = function() {
//对,这个就是核心方法了,给HTML设置font-size。
var innerWidth = document.documentElement.getBoundingClientRect().width || window.innerWidth;
if (hotcss.maxWidth && (innerWidth / hotcss.dpr > hotcss.maxWidth)) {
innerWidth = hotcss.maxWidth hotcss.dpr;
}
if (!innerWidth) {
return false;
}
document.documentElement.style.fontSize = (innerWidth * 20 / 320) + 'px';
hotcss.callback && hotcss.callback();
};
hotcss.mresize();
//直接调用一次
window.addEventListener('resize', function() {
clearTimeout(hotcss.tid);
hotcss.tid = setTimeout(hotcss.mresize, 33);
}, false);
//绑定resize的时候调用
window.addEventListener('load', hotcss.mresize, false);
//防止不明原因的bug。load之后再调用一次。
setTimeout(function() {
hotcss.mresize();
//防止某些机型怪异现象,异步再调用一次
}, 333)
window.hotcss = hotcss;
//命名空间暴露给你,控制权交给你,想怎么调怎么调。
})(window, document);
里面有这样简单粗暴的设置:
dpr = dpr >= 3 ? 3 : ( dpr >=2 ? 2 : 1 );
3. 前端开发博客的方案
<span style="font-family: Arial, Helvetica, sans-serif;">动态写dpr和根元素的fontsize: </span>
!function(x) {
function w() {
var a = r.getBoundingClientRect().width;
a / v > 540 && (a = 540 * v), x.rem = a / 16, r.style.fontSize = x.rem + "px"
}
var v, u, t, s = x.document, r = s.documentElement, q = s.querySelector('meta[name="viewport"]');
if (q) {
console.warn("将根据已有的meta标签来设置缩放比例");
var o = q.getAttribute("content").match(/initial\-scale=(["‘]?)([\d\.]+)\1?/);
o && (u = parseFloat(o[2]), v = parseInt(1 / u))
}
if (!v && !u) {
var n = (x.navigator.appVersion.match(/android/gi), x.navigator.appVersion.match(/iphone/gi)), v = x.devicePixelRatio;
v = n ? v >= 3 ? 3 : v >= 2 ? 2 : 1 : 1, u = 1 / v
}
if (r.setAttribute("data-dpr", v), !q) {
if (q = s.createElement("meta"), q.setAttribute("name", "viewport"), q.setAttribute("content", "initial-scale=" + u + ", maximum-scale=" + u + ", minimum-scale=" + u + ", user-scalable=no"), r.firstElementChild) {
r.firstElementChild.appendChild(q)
} else {
var m = s.createElement("div");
m.appendChild(q), s.write(m.innerHTML)
}
}
x.dpr = v, x.addEventListener("resize", function() {
clearTimeout(t), t = setTimeout(w, 300)
}, !1), x.addEventListener("pageshow", function(b) {
b.persisted && (clearTimeout(t), t = setTimeout(w, 300))
}, !1), "complete" === s.readyState ? s.body.style.fontSize = 12 * v + "px" : s.addEventListener("DOMContentLoaded", function() {
s.body.style.fontSize = 12 * v + "px"
}, !1), w()
}(window);
4. 耿旭亮的方案
直接存为一个 viewport.js 文件,在所有js中最先引入该文件
!function(win, lib) {
var timer,
doc = win.document,
docElem = doc.documentElement,
vpMeta = doc.querySelector('meta[name="viewport"]'),
flexMeta = doc.querySelector('meta[name="flexible"]'),
dpr = 0,
scale = 0,
flexible = lib.flexible || (lib.flexible = {});
// 设置了 viewport meta
if (vpMeta) {
console.warn("将根据已有的meta标签来设置缩放比例");
var initial = vpMeta.getAttribute("content").match(/initial\-scale=([\d\.]+)/);
if (initial) {
scale = parseFloat(initial[1]); // 已设置的 initialScale
dpr = parseInt(1 / scale); // 设备像素比 devicePixelRatio
}
}
// 设置了 flexible Meta
else if (flexMeta) {
var flexMetaContent = flexMeta.getAttribute("content");
if (flexMetaContent) {
var initial = flexMetaContent.match(/initial\-dpr=([\d\.]+)/),
maximum = flexMetaContent.match(/maximum\-dpr=([\d\.]+)/);
if (initial) {
dpr = parseFloat(initial[1]);
scale = parseFloat((1 / dpr).toFixed(2));
}
if (maximum) {
dpr = parseFloat(maximum[1]);
scale = parseFloat((1 / dpr).toFixed(2));
}
}
}
// viewport 或 flexible
// meta 均未设置
if (!dpr && !scale) {
// QST
// 这里的 第一句有什么用 ?
// 和 Android 有毛关系 ?
var u = (win.navigator.appVersion.match(/android/gi), win.navigator.appVersion.match(/iphone/gi)),
_dpr = win.devicePixelRatio;
// 所以这里似乎是将所有 Android 设备都设置为 1 了
dpr = u ? ( (_dpr >= 3 && (!dpr || dpr >= 3))
? 3
: (_dpr >= 2 && (!dpr || dpr >= 2))
? 2
: 1
)
: 1;
scale = 1 / dpr;
}
docElem.setAttribute("data-dpr", dpr);
// 插入 viewport meta
if (!vpMeta) {
vpMeta = doc.createElement("meta");
vpMeta.setAttribute("name", "viewport");
vpMeta.setAttribute("content",
"initial-scale=" + scale + ", maximum-scale=" + scale + ", minimum-scale=" + scale + ", user-scalable=no");
if (docElem.firstElementChild) {
docElem.firstElementChild.appendChild(vpMeta)
} else {
var div = doc.createElement("div");
div.appendChild(vpMeta);
doc.write(div.innerHTML);
}
}
function setFontSize() {
var winWidth = docElem.getBoundingClientRect().width;
if (winWidth / dpr > 540) {
(winWidth = 540 * dpr);
}
// 根节点 fontSize 根据宽度决定
var baseSize = winWidth / 10;
docElem.style.fontSize = baseSize + "px";
flexible.rem = win.rem = baseSize;
}
// 调整窗口时重置
win.addEventListener("resize", function() {
clearTimeout(timer);
timer = setTimeout(setFontSize, 300);
}, false);
// orientationchange 时也需要重算下吧
win.addEventListener("orientationchange", function() {
clearTimeout(timer);
timer = setTimeout(setFontSize, 300);
}, false);
// pageshow
// keyword: 倒退 缓存相关
win.addEventListener("pageshow", function(e) {
if (e.persisted) {
clearTimeout(timer);
timer = setTimeout(setFontSize, 300);
}
}, false);
// 设置基准字体
if ("complete" === doc.readyState) {
doc.body.style.fontSize = 12 * dpr + "px";
} else {
doc.addEventListener("DOMContentLoaded", function() {
doc.body.style.fontSize = 12 * dpr + "px";
}, false);
}
setFontSize();
flexible.dpr = win.dpr = dpr;
flexible.refreshRem = setFontSize;
flexible.rem2px = function(d) {
var c = parseFloat(d) * this.rem;
if ("string" == typeof d && d.match(/rem$/)) {
c += "px";
}
return c;
};
flexible.px2rem = function(d) {
var c = parseFloat(d) / this.rem;
if ("string" == typeof d && d.match(/px$/)) {
c += "rem";
}
return c;
}
}(window, window.lib || (window.lib = {}));