探討判斷橫豎屏的最佳實現

zsw1986 8年前發布 | 10K 次閱讀 瀏覽器 移動開發

在移動端,判斷橫豎屏的場景并不少見,比如根據橫豎屏以不同的樣式來適配,抑或是提醒用戶切換為豎屏以保持良好的用戶體驗。

判斷橫豎屏的實現方法多種多樣,本文就此來探討下目前有哪些實現方法以及其中的優缺點。

CSS Media Queries

通過媒體查詢的方式,我們可以通過以下方法來實現根據橫豎屏不同的情況來適配樣式:

1.內聯樣式

@media screen and (orientation:portrait) {
    //豎屏
}
@media screen and (orientation:landscape) {
    //橫屏
}

2.外聯樣式

<!-- 豎屏 -->
<linkrel="stylesheet" media="all and (orientation:portrait)" href="..." />
<!-- 橫屏 -->
<linkrel="stylesheet" media="all and (orientation:landscape)" href="..." />

window.matchMedia()

除此之外, CSS Object Model(CSSOM)Views 規范增加了對 JavaScript 操作 CSS Media Queries 的原生支持,它在 window 對象下增加了 matchMedia() 方法,讓我們能夠通過腳本的方式來實現媒體查詢。

window.matchMedia() 方法接受一個 Media Queries 語句的字符串作為參數,返回一個 MediaQueryList 對象。該對象有 media 和 matches 兩個屬性:

  • media:返回所查詢的 Media Queries 語句字符串
  • matches:返回一個布爾值,表示當前環境是否匹配查詢語句

同時,它還包含了兩個方法,用來監聽事件:

  • addListener(callback):綁定回調 callback 函數
  • removeListener(callback):注銷回調 callback 函數

那么,通過 window.matchMedia() 的方法,我們可以這樣判斷橫豎屏:

var mql = window.matchMedia("(orientation: portrait)");
function onMatchMeidaChange(mql){
    if(mql.matches) {
        // 豎屏
    }else {
        // 橫屏
    }
}
onMatchMeidaChange(mql);
mql.addListener(onMatchMeidaChange);

通過 Can I Use – matchMeida 可以知道,該API在移動端得到良好的支持,并無兼容性問題。

window.innerHeight/window.innerWidth

The ‘orientation’ media feature is ‘portrait’ when the value of the ‘height’ media feature is greater than or equal to the value of the ‘width’ media feature. Otherwise ‘orientation’ is ‘landscape’.

—— CSS/Mediaqueries/orientation

在 CSS Media Queries 中,Orientation 屬性有兩個值:

  • portrait,指的是當 height 大于等于 width 的情況
  • landscape,指的是當 height 小于 width 的情況

所以,還有一種最為常見的方法是通過比較頁面的寬高,當頁面的高大于等于寬時則認為是豎屏,反之則為橫屏。

function detectOrient(){
    if(window.innerHeight >= window.innerWidth) {
        // 豎屏
    }else {
        // 橫屏
    }
}
detectOrient();
window.addEventListener('resize',detectOrient);

window.orientation

在 iOS 平臺以及大部分 Android 手機都有支持 window.orientation 這個屬性,它返回一個與默認屏幕方向偏離的角度值:

  • 0:代表此時是默認屏幕方向
  • 90:代表順時針偏離默認屏幕方向90度
  • -90:代表逆時針偏離默認屏幕方向90度
  • 180:代表偏離默認屏幕方向180度

在 iOS 的開發者文檔( iOS Developer Library – Handling Orientation Events )是這樣明確定義的:

switch(window.orientation) {
    case 0:
        displayStr += "Portrait";
        break;
    case -90:
        displayStr += "Landscape (right, screen turned clockwise)";
        break;
    case 90:
        displayStr += "Landscape (left, screen turned counterclockwise)";
        break;
    case 180:
        displayStr += "Portrait (upside-down portrait)";
        break;
}

也就是如下圖所示:

(圖來自William Malone – DETECT IOS DEVICE ORIENTATION WITH JAVASCRIPT)

在實際應用中,對于 iPhone 和大部分 Android 是沒有180度的手機豎屏翻轉的情況的,但是 iPad 是存在的。所以,簡化下代碼,我們可以綁定 orientationchange 事件來判斷橫豎屏:

function detectOrient(){
    if (Math.abs(window.orientation) === 90) {
        // 橫屏
    } else {
        // 豎屏
    }
}
detectOrient();
window.addEventListener('orientationchange',detectOrient);

影響判斷的問題所在

1.對window.orientation屬性值的不一致

在 iOS 平臺,對 window.orientation 屬性值是無異議的,規范當中有明確規定每個值對應的情況。但是對于 Android 平臺,就有不一致的特殊情況出現。

A misconception about window.orientation 中作者 Matthew Gifford 就有提到部分 Android 機型(該文章中測試用的 Toshiba Thrive 機型)返回的情況是與期望情況是相反的;除此之外,在 StackOverflow 上也有反饋過這樣的問題

其實,Matthew Gifford 認為這并不是 BUG(筆者也認同),按照 Compatibility Standard – 4.2 window.orientation API 規范中的定義, 0 值指的是 natural 、 default 的屏幕方向,所以如果生廠商對 natural 、 default 狀態是用戶應當手持設備方向為橫屏,那么 0 值對應為 landscape 的橫屏方向了。

針對這種不一致情況的出現,對于追求完美的開發者來說,通過 window.orientation 的方法來判斷橫豎屏則變得有點不可靠的。

2.軟鍵盤的彈出

是否除了 window.orientation 的其它方法都是可靠的呢?

然而,實際上是事與愿違的。在 Android 下,如果頁面中出現軟鍵盤彈出的情況(存在有 Input 的元素)時,頁面有時會因為軟鍵盤的彈出而導致頁面回縮,即頁面的寬度(豎屏時)或者高度(橫屏時)被改變。

無論是 CSS Media Queries 還是 window.matchMedia() 方法,還是根據 window.innerWidth 、 window.innerHeight 的頁面寬高比對方法來實現的橫豎屏判斷方法,都會因此受到影響,出現判斷失誤的情況( Samsung SCH-i699 機型,在豎屏時由于軟鍵盤彈出導致頁面高度小于寬度,被錯誤地判定為橫屏)。

所以,在這樣的情況下,這幾種方式也變得不可靠。

探討最佳實現方式

本著核心的原則——具體情況具體解決來討論。

如果你沒有遇上以上兩個問題所在,恭喜你!上面所提到的方法都可以被應用,選擇你最為喜歡的方法就好。

但是如果想要避免以上兩個問題所在,有沒有更好的辦法呢?

經過實際情況的研究,針對開發環境兼容的情況( iOS 與 Android 下的微信內置瀏覽器與原生瀏覽器)來說,屏幕分辨率是不會改變的,那么我們可以嘗試比對頁面寬高和屏幕分辨率來判斷橫豎屏。

需要注意的是,微信內置瀏覽器頁面寬度不包括頂欄部分的,而 Android 和 iOS 的原生瀏覽器都是帶有底欄或頂欄兼有的,如下圖所示。

(圖為 iPhone 6s 下的微信內置瀏覽器與原生瀏覽器截圖)

那么,我們可以確定為:

假如屏幕分辨率固定值為: screen.width 和 screen.height (

需要注意,這里很重要的一點是: 在移動端,屏幕翻轉時, screen.width 和 screen.height 的值依然是不變的  。這點有誤,后面有補充修正

  • 若獲取 當前頁面的寬( document.documentElement.clientWidth ),等于屏幕分辨率的寬( screen.width ),則可認定當前屬于 豎屏

(圖為以 iPhone 6s 豎屏下的微信內置瀏覽器為例的截圖)

  • 若獲取 當前頁面的寬( document.documentElement.clientWidth ),等于屏幕分辨率的高( screen.height ),則可認定當前屬于 橫屏

(圖為以 iPhone 6s 橫屏下的微信內置瀏覽器為例的截圖)

如此,對應的代碼為:

function detectOrient() {
    var storage = localStorage;
    var data = storage.getItem('J-recordOrientX');
    var w = document.documentElement.clientWidth,
        h = document.documentElement.clientHeight;
    var _Width = 0,
        _Height = 0;
    if(!data) {
        _Width = window.screen.width;
        _Height = window.screen.height;
        storage.setItem('J-recordOrientX',_Width + ',' + _Height);
    }else {
        var str = data.split(',');
        _Width = str[0];
        _Height = str[1];
    }
    if(w == _Width) {
        // 豎屏
        return;
    }
    if(w == _Height){
        // 橫屏
        return;
    }
}
detectOrient();
window.addEventListener('resize',detectOrient);

以上是筆者拙劣的見解,如果你有更好的辦法解決,歡迎來分享!

今后的發展

目前,W3C 引入 Screen Orientation API ,該標準能夠幫助 Web 應用獲得屏幕方向的狀態,在狀態改變時獲得通知,并能夠從應用程序中將屏幕狀態鎖定到特定狀態。

但截止目前,該標準仍在 W3C 草案階段。在移動端,它在 Android 和 iOS 平臺上仍未得到支持,僅僅在 Chrome for Android 39 版本及以上才得到實現,所以對目前的開發來說意義不大。只能期待它能夠盡快通過并得到廣泛支持,這樣的檢測屏幕方向的問題就能夠得到規范化的解決。

20170425更新

感謝各位讀者的反饋,筆者的自測確實是沒有覆蓋全面,有些讀者反饋的以下幾點問題確實存在:

1.在 華為P9 的微信(6.5.4)、華為榮耀的微信(6.5.7)和 Chrome 瀏覽器上, screen.width 與 screen.height 均會隨著橫豎屏的切換而變。

2.另外,筆者也發現在移動端還有一點很重要的點會影響到 document.documentElement.clientWidth/clientHeight 的值 —— Meta Viewport的設置。

3.在微信內(其他移動瀏覽器也會),會多次觸發resize事件。

然而,以上三個問題都是不影響本文所提出的方法的核心思想,而只需要將方法進行bug的修正即可。

這里先丟出體驗地址,其中,修正后的源碼如下:

// 判斷橫豎屏
var utils = {
    debounce: function(func,delay){
        var timer = null;
        return function(){
            var context = this,
                args = arguments;
            clearTimeout(timer);
            timer = setTimeout(function(){
                func.apply(context,args);
            },delay);
        }
    }
}
var detectRes = document.getElementById('J_detectRes');
var detectData = document.getElementById('J_detectData');
function detectOrient() {
    var storage = localStorage; // 不一定要使用localStorage,其他存儲數據的手段都可以
    var data = storage.getItem('J-recordOrientX');
    var cw = document.documentElement.clientWidth;
    var _Width = 0,
        _Height = 0;
    if(!data) {
        sw = window.screen.width;
        sh = window.screen.height;
        // 2.在某些機型(如華為P9)下出現 srceen.width/height 值交換,所以進行大小值比較判斷
        _Width = sw < sh ? sw : sh;
        _Height = sw >= sh ? sw : sh;
        storage.setItem('J-recordOrientX',_Width + ',' + _Height);
    }else {
        var str = data.split(',');
        _Width = str[0];
        _Height = str[1];
    }
    if(cw == _Width) {
        // 豎屏
        return;
    }
    if(cw == _Height){
        // 橫屏
        return;
    }
}
// 3.函數去抖處理
window.onresize = utils.debounce(detectOrient,300);
detectOrient();

然后,下面講解如何針對性逐一突破。

1.橫豎屏切換時, screen.width 與 screen.height 的值可能會改變

隨著橫豎屏幕的切換, screen.width 與 screen.height 在大部分機型上會維持不變,而在一些機型上如@Jc、@百思不得姐夫

提出的華為 P9 微信內置瀏覽器(6.5.4版本)、Chrome桌面端瀏覽器模擬器中會出現值交換的現象。

例如,在Chome上 iPhone 6 模擬器中,豎屏時 screen.width 與 screen.height 等于375px、667px,而橫屏時, sreen.width 與 screen.height 等于 667px 、 375px,兩者屬性值出現了值交換現象。

這個問題很容易解決,雖然出現了值交換,但是值大小還是不變的,那么我們可以先通過比較大小來判斷出屬性值較小的是 screen.width ,而屬性值較大的是 screen.height ,然后再用來與 document.documentElement.clientWidth/clientHeight 進行比較,從而判斷出橫豎屏。

2.Meta Viewport的設置會影響 document.documentElement.clientWidth/clientHeight

Peter-Paul Koch 的 《兩個 Viewport 的故事》 的一文中提出的關于 Viewport 的理論被認為是業界的主流論調,它指出 Layout Viewport 的尺寸可以通過 document.documentElement.clientWidth/clientHeight 進行度量。而通過設置 Meta Viewport (也就是 viewport meta 標簽)是可以改變 Layout Viewport 的尺寸。

所以,Meta Viewport的屬性設置如何是會影響到 document.documentElement.clientWidth/clientHeight 的值,這就是一部分讀者迷惑到”為什么會我測量 document.documentElement.clientWidth/clientHeight 的值與 screen.width/height 的值不相同?“的原因所在。

因此,在這里也補充一點,在筆者提出的方法中,有個忘記跟大家說明的前提——頁面設置了以下屬性以保證頁面的適配:

<metaname="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0;" />

這句語句的設置就保證了頁面是始終適配屏幕的,使得在橫豎屏切換的場景中 document.documentElement.clientWidth/clientHeight 必然與 screen.width/height 其中一值相等,并且這也是本文提出的橫豎屏檢測方法的核心。

3.resize事件的多次觸發

筆者是通過綁定監聽resize事件來響應執行橫豎屏檢測方法的,而在實際應用中確實出現了resize事件觸發兩次的情況。

雖然并沒有影響到事件的判斷結果,但是這也算個值得優化的點,而且問題也不大,我們只要通過 函數去抖( Debounce Function ) 辦法來進行簡單的解決就好。

// 函數去抖的簡單封裝
var utils = {
    debounce: function(func,delay){
        var timer = null;
        return function(){
            var context = this,
                args = arguments;
            clearTimeout(timer);
            timer = setTimeout(function(){
                func.apply(context,args);
            },delay);
        }
    },
    ...
}

參考文檔

 

來自:http://jdc.jd.com/archives/3862

 

 本文由用戶 zsw1986 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!