移动端web开发——像素的适配

像素和dpr

定义

像素,又称画素,是图像显示的基本单位,译自英文“pixel”,pix 是英语单词 picture 的常用简写,加上英语单词“元素” element,就得到 pixel,故“像素”表示“图像元素”之意,有时亦被称为 pel(picture element)

像素是网页布局的基础。一个像素就是计算机能够显示一种特定颜色的最小区域。当设备尺寸相同但像素变得更密集时,屏幕能显示的画面的过渡更细致,网站看起来更明快。

ppi 是指屏幕上每英寸可以显示的像素点的数量,即屏幕像素密度

分类

像素分为两种:设备像素和 CSS 像素

  1. 设备像素(device independent pixels): 设备屏幕的物理像素,任何设备的物理像素的数量都是固定的
  2. CSS 像素(CSS pixels): 又称为逻辑像素,是为web开发者创造的,在 CSS 和 javascript 中使用的一个抽象的层

CSS 声明和几乎所有的 javascript 属性都使用 CSS 像素,因此实际上从来用不上设备像素,唯一的例外是screen.width/height

//我们通过CSSjavascript代码设置的像素都是逻辑像素
width:300px;
font-size:16px;
缩放

(设备像素→深蓝色背景,CSS 像素→半透明背景)

在桌面端,css 的1个像素往往都是对应着电脑屏幕的1个物理像素。

//一个 CSS 像素完全覆盖了一个设备像素

image

而在手机端,由于屏幕尺寸的限制,缩放是经常性的操作。

//左图表示当用户进行缩小操作时,一个设备像素覆盖了多个 CSS 像素

//右图表示当用户进行放大操作时,一个 CSS 像素覆盖了多个设备像素

image image

不论我们进行缩小或放大操作,元素设置的 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设备PPIDPR
iPhone43262
iPhone4s3262
iPhone53262
iPhone5s3262
iPhone63262
iPhone6s3262
iPhone6 PLUS4413
iPhone6s PLUS4413
iPad --2
iPad --3
android尺寸dpr
低清设备1
-1.5
-1.75
-2
mx2800*12802.5
小米note720*12802.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);
        //允许通过自定义namehotcssmeta头,通过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);
    //防止不明原因的bugload之后再调用一次。
    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 = {}));
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值