了解真實的『REM』手機屏幕適配

rem 作為一個低調的長度單位,由于手機端網頁的興起,在屏幕適配中得到重用。使用 rem 前端開發者可以很方便的在各種屏幕尺寸下,通過等比縮放的方式達到設計圖要求的效果。

rem 的官方定義『The font size of the root element.』,即以根節點的字體大小作為基準值進行長度計算。一般認為網頁中的根節點是 html 元素,所以采用的方式也是通過設置 html 元素的 font-size 來做屏幕適配,但實際情況真有這么簡單嗎?

首先我們來看看使用 rem 實現手機屏幕適配的常用方案。

以設計稿的寬度為640px,即:designWidth = 640,同時設定在640px屏寬下 1rem=100px ,即:rem2px = 100

設置 1rem=100px 的優點不言而喻。前端開發者在切圖、重構頁面的時候,通過直接位移小數點的方式,就可以將UI圖中測量到的 px 值換算成對應的 rem 值,方便快捷。

此外,在 head 中我們還設置了:<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0" />

viewport 的作用很重要,但不是本文的重點所以不展開,有興趣的同學可以自行搜索。

先來看看具體方案:

下面四個方案來自同事共享,原理都是采用等比縮放的方式 —— 獲得目標屏幕寬度和設計稿寬度的比,作為 rem 的基值(縮放系數),設置為html標簽的字體大小。不同的只是在于性能取舍和書寫習慣。

方案1

@media screen and (min-width: 320px) {html{font-size:50px;}}
@media screen and (min-width: 360px) {html{font-size:56.25px;}}
@media screen and (min-width: 375px) {html{font-size:58.59375px;}}
@media screen and (min-width: 400px) {html{font-size:62.5px;}}
@media screen and (min-width: 414px) {html{font-size:64.6875px;}}
@media screen and (min-width: 440px) {html{font-size:68.75px;}}
@media screen and (min-width: 480px) {html{font-size:75px;}}
@media screen and (min-width: 520px) {html{font-size:81.25px;}}
@media screen and (min-width: 560px) {html{font-size:87.5px;}}
@media screen and (min-width: 600px) {html{font-size:93.75px;}}
@media screen and (min-width: 640px) {html{font-size:100px;}}
@media screen and (min-width: 680px) {html{font-size:106.25px;}}
@media screen and (min-width: 720px) {html{font-size:112.5px;}}
@media screen and (min-width: 760px) {html{font-size:118.75px;}}
@media screen and (min-width: 800px) {html{font-size:125px;}}
@media screen and (min-width: 960px) {html{font-size:150px;}}

方案2

@media screen and (min-width: 320px) {html{font-size:312.5%;}}
@media screen and (min-width: 360px) {html{font-size:351.5625%;}}
@media screen and (min-width: 375px) {html{font-size:366.211%;}}
@media screen and (min-width: 400px) {html{font-size:390.625%;}}
@media screen and (min-width: 414px) {html{font-size:404.2969%;}}
@media screen and (min-width: 440px) {html{font-size:429.6875%;}}
@media screen and (min-width: 480px) {html{font-size:468.75%;}}
@media screen and (min-width: 520px) {html{font-size:507.8125%;}}
@media screen and (min-width: 560px) {html{font-size:546.875%;}}
@media screen and (min-width: 600px) {html{font-size:585.9375%;}}
@media screen and (min-width: 640px) {html{font-size:625%;}}
@media screen and (min-width: 680px) {html{font-size:664.0625%;}}
@media screen and (min-width: 720px) {html{font-size:703.125%;}}
@media screen and (min-width: 760px) {html{font-size:742.1875%;}}
@media screen and (min-width: 800px) {html{font-size:781.25%;}}
@media screen and (min-width: 960px) {html{font-size:937.5%;}}

方案3

var designWidth = 640, rem2px = 100;
document.documentElement.style.fontSize = 
  ((window.innerWidth / designWidth) * rem2px) + 'px';

方案4

var designWidth = 640, rem2px = 100;
document.documentElement.style.fontSize = 
  ((((window.innerWidth / designWidth) * rem2px) / 16) * 100) + '%';

為了更避免理解上的混亂,我在上面js的代碼中加了 ( ) ,實際代碼中是不需要的。

詳細分析一下,rempx 直接的轉換公式可以寫為:

1rem = 1 * htmlFontSize

htmlFontSizehtml 元素的字體大小。

首先來看方案1中,在屏寬為640px情況下的設置:

@media screen and (min-width: 640px) {html{font-size:100px;}}

可以很明顯的表現出這一點 1rem = 1 * 100px ,同我們最初的設定。那么我們要得到其它屏幕大小的 htmlFontSize 值要怎么辦。很簡單如方案3,因為我們的采用等比縮放的方式適配,所以計算目標屏幕寬度和設計稿的寬度的比即可:

window.innerWidth / designWidth * rem2px + 'px'

由于瀏覽器默認字體大小為 16px,所以當我們使用百分比作為根節點 html 的字體大小時,即html元素的font-size值設置為一個百分比值,rem 的計算方式就會改為:

defaultFontSize = 16px
1rem = 1 * htmlFontSize * defaultFontSize

如方案2中,在屏寬為640px情況下的設置:

@media screen and (min-width: 640px) {html{font-size:625%;}}

應用上面的公式:

1rem = 1 * 625% * 16px
其中:625% * 16 = 6.25 * 16 = 100
所以:1rem = 1 * 100px

同樣的可以得到所有屏幕大小下,htmlfont-size 值的計算公式,即為方案4:

window.innerWidth / designWidth * rem2px / 16 * 100  + '%'

通過方案3和方案4的公式,就可以很方便的生成方案1和方案2中的css。

這里只給出了方案3和方案4對應驗證頁面(方案1和方案2是它們的變形): scheme3.html, scheme4.html

如下面兩張圖,是在屏寬為360px下的效果,通過計算目標為:1rem = 56.25px。方案3設置值為:56.25px,方案4設置值為:351.5625%

方案3 方案4

到目前為止貌似很完美的解決了問題,實際情況當然是出現了意外。在有些 Android 手機上,瀏覽器或 webview 的默認字體是隨著系統設置的字體改變的。這樣就會導致默認字體大于或小于 16px

修改默認字體大小后,我們再看方案3和方案4。

同樣在屏寬為360px下,我們調大系統字體大小,如下面的效果

設置前 html 元素的字體大小的計算值18px ,設置后的計算值65px ,由于屏幕寬度沒有改變,我們的目標值,即我們在 html 元素上設置的 font-size 值也沒有變化任然為 56.25px,而最終計算值出現了偏差。

方案3 方案4

分析偏差前,先來看在360px屏寬下,方案3和方案4的計算過程:

方案3:

document.documentElement.style.fontSize = 56.25px
htmlFontSize = 56.25px
1rem = 1 * htmlFontSize = 56.25px
實際為:
1rem = 64.6875px

方案4:

document.documentElement.style.fontSize = 351.5625%
htmlFontSize = 351.5625%
defaultFontSize = 18px
1rem = 1 * htmlFontSize * defaultFontSize = 351.5625% * 18px = 63.28125px
351.5625% * 18 = 63.28125
實際為:
1rem = 64.6875px

貌似方案4的計算結果很接近實際效果,而方案3偏差很大。再來比較方案3和方案4的計算公式:

// 方案3
document.documentElement.style.fontSize = 
  window.innerWidth / designWidth * rem2px + 'px';

// 方案4
document.documentElement.style.fontSize = 
  window.innerWidth / designWidth * rem2px / 16 * 100 + '%';

方案4較于方案3其實多除了一個16,可以推測瀏覽器在計算 rem 的具體值時,如果 html 設置的 font-sizepx 值時會先除以 16 ,然后再乘以 htmlFontSize

1rem = 1 * (56.25px / 16) * 18
1 * (56.25 / 16) * 18 = 63.28125

方案4存在問題,是因為系統的默認字體改為了 18px ,但是我們在計算百分比是時候,還是以 16px 為基準值進行計算,所以出現偏差(計算值和實際值之間還有一點偏差這個在后面會提到)。

而在方案3中,我們其實是不考慮瀏覽器默認字體大小的,但在實際使用的過程中,瀏覽器還是除了 16 ,而此時默認字體大小為 18px。得出如下在 htmlfontSize 設置為 px 的情況下 rem 的計算公式為:

1rem = 1 * (htmlFontSize / 16) * defaultFontSize

在系統設置的字體大小發生改變時,defaultFontSize 會跟著改變,而 16 不會變化。所以方案3雖然表面上不考慮默認字體大小的變化,只關注屏幕與設計稿之間的寬度比,但在實際計算中還是使用到了默認字體大小,而且還有一個不變的 16 在作祟,導致方案3失敗。

所謂的「root element」其實不是想象的那樣,一個是16,一個是18,到底取的是那個 root element 的字體大小。

ok,rem 的計算的時候,px 的方式會有一個16不隨系統字體大小改變,所以我們采用百分比的方案,繞開這個問題。

采用百分比的方案4因為在計算時寫死了默認字體大小 16px。所以它的偏差在于沒能動態的獲取默認字體大小。更新如下:

方案4.1

var designWidth = 640, rem2px = 100;
var h = document.getElementsByTagName('html')[0];
var htmlFontSize = parseFloat(window.getComputedStyle(h, null)
                                    .getPropertyValue('font-size'));

document.documentElement.style.fontSize = 
  window.innerWidth / designWidth * rem2px / htmlFontSize * 100 + '%';

效果如下圖:

16px 的圖中,設置后的 htmlfont-size1rem 的實際值有偏差,同時 6.4rem 的計算值也有偏差。通過查看代碼發現htmlfont-size使用的是: getPropertyValue('font-size')1rem 使用的是 getPropertyValue('width'),偏差出在計算 font-size 的時候瀏覽器進行了四舍五入。rem 定義中的另一個元素「font size」也不能按字面意思使用,宣告失守。

18px 中的偏差,以及上文中方案4在 18px 實際值和計算值出現的偏差都是同樣的問題。所以基準值還需要修改。

16px 18px

在更新一版,方案4.2:

var designWidth = 640, rem2px = 100;
var d = window.document.createElement('div');
d.style.width = '1rem';
d.style.display = "none";
var head = window.document.getElementsByTagName('head')[0];
head.appendChild(d);
var defaultFontSize = parseFloat(window.getComputedStyle(d, null).getPropertyValue('width'));
// d.remove();
document.documentElement.style.fontSize = 
  window.innerWidth / designWidth * rem2px / defaultFontSize * 100 + '%';

效果如下圖:

16px 18px

到此為止,rem 在默認字體不是 16px 的情況下的處理已經解決,考慮到還有設計屏幕旋轉,最終手機端的解決方案為:

function adapt(designWidth, rem2px){
  var d = window.document.createElement('div');
  d.style.width = '1rem';
  d.style.display = "none";
  var head = window.document.getElementsByTagName('head')[0];
  head.appendChild(d);
  var defaultFontSize = parseFloat(window.getComputedStyle(d, null).getPropertyValue('width'));
  // d.remove();
  // document.documentElement.style.fontSize = window.innerWidth / designWidth * 
rem2px / defaultFontSize * 100 + '%';
  var st = document.createElement('style');
  var portrait = "@media screen and (min-width: "+window.innerWidth+"px) 
{html{font-size:"+ ((window.innerWidth/(designWidth/rem2px)/defaultFontSize)*100) +"%;}}";
  var landscape = "@media screen and (min-width: "+window.innerHeight+"px) 
{html{font-size:"+ ((window.innerHeight/(designWidth/rem2px)/defaultFontSize)*100) +"%;}}"
  st.innerHTML = portrait + landscape;
  head.appendChild(st);
  return defaultFontSize
};
var defaultFontSize = adapt(640, 100);

作者:hbxeagle
查看原文

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,401評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,011評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,263評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,543評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,323評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,874評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,968評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,095評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,605評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,551評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,720評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,242評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,961評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,358評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,612評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,330評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,690評論 2 370

推薦閱讀更多精彩內容