判断移动设备的横屏和竖屏最佳方案

之前写的文章:使用JavaScript判断手机是处于横屏还是竖屏,最近的一个项目需要判断这个,但在chrome的调试模式中发现横屏和竖屏使用上面的代码并不能检测到,所以我认真做了一些了解,本文分享一个合适的代码来判断横屏和竖屏。

存在的问题

1.有些移动设备不支持这个 window.orientation 这个属性,导致没法检测。

2.有些设备横屏和竖屏并不一定代表横屏就是宽度最大,竖屏就是高度大的,横屏和竖屏的定义是有厂商规定那个正面的屏幕方向。

由此带来的问题,无法检测到到底哪个方向才是我们期望知道的横屏和竖屏,可能导致我们的显示效果出错,当然这个还是不多的,但不能排除。

下面分享实际中有哪些方法实现以及他们的优缺点,最后会给出一个最佳解决方案。

CSS Media Queries

通过媒体查询的方式,我们可以通过以下方法来实现根据横竖屏不同的情况来适配样式:

1.内联样式

@media screen and (orientation:portrait) {
    //竖屏
}
@media screen and (orientation:landscape) {
    //横屏
}

2.外联样式

<!-- 竖屏 -->
<link rel="stylesheet" media="all and (orientation:portrait)" href="..." />
<!-- 横屏 -->
<link rel="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() 的方法,我们可以这样判断横竖屏:

那么,通过 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;
}

也就是如下图所示:

iPhone-orientation
(图来自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 上也有反馈过这样的问题(例如,window.orientation returns different values in iOS and Android中提到的 Samsung Tab 2 机型)。

其实,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.innerWidthwindow.innerHeight的页面宽高比对方法来实现的横竖屏判断方法,都会因此受到影响,出现判断失误的情况( Samsung SCH-i699 机型,在竖屏时由于软键盘弹出导致页面高度小于宽度,被错误地判定为横屏)。
所以,在这样的情况下,这几种方式也变得不可靠。

探讨最佳实现方式

假如屏幕分辨率固定值为:screen.widthscreen.height

  1. 若获取 当前页面的宽(document.documentElement.clientWidth),等于屏幕分辨率的宽(screen.width),则可认定当前属于竖屏
  2. 若获取 当前页面的宽(document.documentElement.clientWidth),等于屏幕分辨率的高(screen.height),则可认定当前属于横屏

对应的代码如下(更新后的代码),demo体验

// 判断横竖屏
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.widthscreen.height的值可能会改变

随着横竖屏幕的切换,screen.widthscreen.height在大部分机型上会维持不变,而在一些机型上如@Jc、@百思不得姐夫
提出的华为 P9 微信内置浏览器(6.5.4版本)、Chrome桌面端浏览器模拟器中会出现值交换的现象。

例如,在Chome上 iPhone 6 模拟器中,竖屏时screen.widthscreen.height等于375px、667px,而横屏时,sreen.widthscreen.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的值不相同?“的原因所在。

因此,在这里也补充一点,在笔者提出的方法中,有个忘记跟大家说明的前提——页面设置了以下属性以保证页面的适配:

1
<meta name="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 ) 办法来进行简单的解决就好。

详情参考:凹凸实验室 探讨判断横竖屏的最佳实现