REM 手機屏幕高清適配方案
REM適配主要解決在不同屏幕寬度下,布局和元素尺寸保持一致,即屏寬大的對應尺寸也大。而單獨使用這個方案在要求更高的『高清適配』中,就有些力不從心了。
先來簡單了解一下什么是『DPR』,以及 DPR 帶來的影響。
DPR:當前顯示設備上的一個物理像素的尺寸與一個設備獨立像素的尺寸(dips)的比率。
直白一點,對前端開發來說, 如果不進行特殊設置 ,就是在 dpr = 1 的設備上,css中設置1px,對應顯示在屏幕上也是1像素(物理像素)。 dpr = 2 的設備上(如iPhone6),css中設置1px,對應顯示在屏幕上會是2像素(物理像素)。同理,在 dpr = 3 的設備上如(iPhone6 Plus),css中設置1px,對應顯示在屏幕上會是3像素(物理像素)。至于詳細的關系,這里就不多說了,網上有很多關于 dpr 的說明。
現在來看『高清適配』要解決兩個問題:
- 1px問題:即在不同高清屏幕下,設置為1px,希望顯示也為1像素(物理像素)。尤其是在邊框上此問題比較突出。
- 高清圖片適配:將一張 100px * 100px 的圖片顯示在 100px * 100px 的區域時,由于在 dpr = 2 的設備上,對應的區域其實有 200 * 200 個像素(物理像素)點,所以在此設備上,這張圖片相當于被拉伸了一倍,顯示效果下降。因此要做到高清適配,我們需要在這樣的設備上使用一張 200px * 200px 的圖片,才能保證顯示效果。反過來,如果統一使用 200px * 200px 的圖片,則會造成流量浪費。
問題清楚了,下面直接放出解決方案:
(function(designWidth, rem2px) {
var win = window;
var doc = win.document;
var docEl = doc.documentElement;
var metaEl = doc.querySelector('meta[name="viewport"]');
var dpr = 0;
var scale = 0;
var tid;
if (!dpr && !scale) {
var devicePixelRatio = win.devicePixelRatio;
if (win.navigator.appVersion.match(/iphone/gi)) {
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;
}
docEl.setAttribute('data-dpr', dpr);
if (!metaEl) {
metaEl = doc.createElement('meta');
metaEl.setAttribute('name', 'viewport');
metaEl.setAttribute('content', 'width=device-width,initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
if (docEl.firstElementChild) {
docEl.firstElementChild.appendChild(metaEl);
} else {
var wrap = doc.createElement('div');
wrap.appendChild(metaEl);
doc.write(wrap.innerHTML);
}
} else {
metaEl.setAttribute('name', 'viewport');
metaEl.setAttribute('content', 'width=device-width,initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
}
// 以上代碼是對 dpr 和 viewport 的處理,代碼來自 lib-flexible。
// 一下代碼是處理 rem,來自上篇文章。不同的是獲取屏幕寬度使用的是
// document.documentElement.getBoundingClientRect
// 也是來自 lib-flexible ,tb的技術還是很強啊。
function refreshRem(_designWidth, _rem2px){
// 修改viewport后,對網頁寬度的影響,會立刻反應到
// document.documentElement.getBoundingClientRect().width
// 而這個改變反應到 window.innerWidth ,需要等較長的時間
// 相應的對高度的反應,
// document.documentElement.getBoundingClientRect().height
// 要稍微慢點,沒有準確的數據,應該會受到機器的影響。
var width = docEl.getBoundingClientRect().width;
var d = window.document.createElement('div');
d.style.width = '1rem';
d.style.display = "none";
docEl.firstElementChild.appendChild(d);
var defaultFontSize = parseFloat(window.getComputedStyle(d, null).getPropertyValue('width'));
d.remove();
var portrait = "@media screen and (width: "+ width +"px) {html{font-size:"+ ((width/(_designWidth/_rem2px)/defaultFontSize)*100) +"%;}}";
var dpStyleEl = doc.getElementById('dpAdapt');
if(!dpStyleEl) {
dpStyleEl = document.createElement('style');
dpStyleEl.id = 'dpAdapt';
dpStyleEl.innerHTML = portrait;
docEl.firstElementChild.appendChild(dpStyleEl);
} else {
dpStyleEl.innerHTML = portrait;
}
// 由于 height 的響應速度比較慢,所以在加個延時處理橫屏的情況。
setTimeout(function(){
var height = docEl.getBoundingClientRect().height;
var landscape = "@media screen and (width: "+ height +"px) {html{font-size:"+ ((height/(_designWidth/_rem2px)/defaultFontSize)*100) +"%;}}"
var dlStyleEl = doc.getElementById('dlAdapt');
if(!dlStyleEl) {
dlStyleEl = document.createElement('style');
dlStyleEl.id = 'dlAdapt'
dlStyleEl.innerHTML = landscape;
docEl.firstElementChild.appendChild(dlStyleEl);
} else {
dlStyleEl.innerHTML = landscape;
}
},500);
}
// 延時,讓瀏覽器處理完viewport造成的影響,然后再計算root font-size。
setTimeout(function(){
refreshRem(designWidth, rem2px);
}, 1);
})(640, 100);
此方案分為上下兩部分。
第一部分為通過設置 viewport ,將物理像素和css中的像素對應起來。iOS下,對于2和3的屏,正常按dpr實際值處理,其余的用1倍方案。
- 在 dpr = 1 的設備上設置:
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />
如 Galaxy S5 中,設置前獲取屏寬 window.innerWidth 為 360 ,設置后還是 360 。
- 在 dpr = 2 的設備上:
<meta name="viewport" content="width=device-width, initial-scale=0.5, minimum-scale=0.5, maximum-scale=0.5, user-scalable=no" />
如 iPhone6 中,設置前獲取屏寬 window.innerWidth 為 375 ,設置后是 750 (與物理像素匹配)。
- 在 dpr = 3 的設備上:
<meta name="viewport" content="width=device-width, initial-scale=0.3333333333333333, maximum-scale=0.3333333333333333, minimum-scale=0.3333333333333333, user-scalable=no" />
如 iPhone6 Plus 中,設置前獲取屏寬 window.innerWidth 為 414 ,設置后是 1242 (與物理像素匹配)。
同時在html標簽上設置 「data-dpr」屬性,標注當前設備的dpr值,為后面做圖片高清適配做準備。
第二部分為設置了 viewport 后,獲取新的屏幕高寬,然后按照等比縮放原則,考慮系統字體大小的影響(前一篇中解決的問題),計算出我們需要的 root font size。
注意一定是要在設置的 viewport 生效后,再進行計算,不然獲取到的屏幕高寬不準確。所以在這里使用了兩個延時來保證這一點。
第一個延時,讓瀏覽器處理完修改 viewport 造成的影響,然后再計算root font-size。
setTimeout(function(){
refreshRem(designWidth, rem2px);
}, 1);
第二個延時,由于 height 對 viewport 的更改響應速度比較慢,所以在加個延時處理橫屏的情況。
setTimeout(function(){
var height = docEl.getBoundingClientRect().height;
...
});
另外,最好將上面的代碼用內聯的方式,直接放置到頁面的 head 里面,由于js處理會阻塞頁面渲染(因為寬度對 viewport 的更改相應很快,所以不必擔心上面的代碼占用太長的時間),利用這一點,可以保證后面的頁面渲染的時候不會閃爍。
這樣在css中你就可以放心的使用 1px 來設置 1像素的邊框了。而高清圖片的適配,就需要使用下面的代碼
.pic {
width:1rem;
height:1rem;
background: url(../images/100_100.png);
background-repeat: no-repeat;
background-size: cover;
}
[data-dpr="2"] .pic {
background-image: url("../images/100_100@2x.png");
}
[data-dpr="3"] .pic {
background-image: url("../images/100_100@3x.png");
}
這時候使用 less 或 sass 就很方便了
只給出了替換 .png 的示例。
less:
.dpr(@selector, @img) {
[data-dpr="2"] {
@_img : replace("@{img}", '\.png$' ,'@2x.png');
@{selector} {
background-image: url("@{_img}");
}
}
[data-dpr="3"] {
@_img : replace("@{img}", '\.png$' ,'@3x.png');
@{selector} {
background-image: url("@{_img}");
}
}
}
.pic {
width:1rem;
height:1rem;
background: url(../images/100_100.png);
background-repeat: no-repeat;
background-size: cover;
}
// 這里要加使用 ~".pic",
// 不然生成的 selector 會是帶有引號的 [data-dpr="2"] ".pic"
// 而不是 [data-dpr="2"] .pic
.dpr(~".pic", "../images/100_100.png");
sass:
@function str-replace($string, $search, $replace: '') {
$index: str-index($string, $search);
@if $index {
@return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace);
}
@return $string;
}
@mixin dpr($selector, $img) {
[data-dpr="2"] {
$_img : str-replace("#{$img}", '\.png' ,'@2x.png');
#{$selector} {
background-image: url("#{$_img}");
}
}
[data-dpr="3"] {
$_img : str-replace("#{$img}", '\.png' ,'@3x.png');
#{$selector} {
background-image: url("#{$_img}");
}
}
}
.pic {
width:1rem;
height:1rem;
background: url(../images/100_100.png);
background-repeat: no-repeat;
background-size: cover;
}
@include dpr($selector:".pic", $img:"../images/100_100.png")
另外附贈一個webp特性檢查,也可以一起放到上面的立即執行函數中,同時更新一下 less 或 sass ,就可以很方便使用webp了。
js
// 監測webp支持情況,如果支持為html標簽添加屬性:data-webp=1
(function() {
var webp = new Image();
webp.onload = webp.onerror = function() {
webp.height === 2 && document.documentElement.setAttribute('data-webp', 1);
webp.onload = webp.onerror = null;
webp = null;
};
webp.src = 'data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA';
})();
less
.dpr-webp(@selector, @img) {
[data-dpr="2"] {
@_img : replace("@{img}", '\.png$' ,'@2x.png');
@{selector} {
background-image: url("@{_img}");
}
}
[data-dpr="3"] {
@_img : replace("@{img}", '\.png$' ,'@3x.png');
@{selector} {
background-image: url("@{_img}");
}
}
[data-webp="1"] {
@_img : replace("@{img}", '\.png$' ,'.webp');
@{selector} {
background-image: url("@{_img}");
}
}
[data-webp="1"][data-dpr="2"] {
@_img : replace("@{img}", '\.png$' ,'@2x.webp');
@{selector} {
background-image: url("@{_img}");
}
}
[data-webp="1"][data-dpr="3"] {
@_img : replace("@{img}", '\.png$' ,'@3x.webp');
@{selector} {
background-image: url("@{_img}");
}
}
}
sass
@function str-replace($string, $search, $replace: '') {
$index: str-index($string, $search);
@if $index {
@return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace);
}
@return $string;
}
@mixin dpr-webp($selector, $img) {
[data-dpr="2"] {
$_img : str-replace("#{$img}", '\.png' ,'@2x.png');
#{$selector} {
background-image: url("#{$_img}");
}
}
[data-dpr="3"] {
$_img : str-replace("#{$img}", '\.png' ,'@3x.png');
#{$selector} {
background-image: url("#{$_img}");
}
}
[data-webp="1"] {
$_img : str-replace("#{$img}", '\.png' ,'.webp');
#{$selector} {
background-image: url("#{$_img}");
}
}
[data-webp="1"][data-dpr="2"] {
$_img : str-replace("#{$img}", '\.png' ,'@2x.webp');
#{$selector} {
background-image: url("#{$_img}");
}
}
[data-webp="1"][data-dpr="3"] {
$_img : str-replace("#{$img}", '\.png' ,'@3x.webp');
#{$selector} {
background-image: url("#{$_img}");
}
}
}
結語由于要處理『高清適配』同時兼容Android在font size的bug,所以過程有些繞。另外,不直接使用 lib-flexible 是有兩點,一方面不習慣它的類vw的尺寸書寫方式(與1vw對應屏寬類似,使用10rem對應屏寬,開發時需要使用插件將測量值換算為目標值),另一方面它也沒去解決Android的font size的bug。
來自:https://github.com/hbxeagle/rem/blob/master/HD_ADAPTER.md