探讨h5横竖屏的最佳实现方案
在移动端,判断横竖屏的场景并不少见,比如根据横竖屏以不同的样式来适配,抑或是提醒用户切换为竖屏以保持良好的用户体验。
判断横竖屏的实现方法多种多样,现在就来探讨下目前有哪些实现方法以及其中的优缺点。
The orientation CSS media feature can be used to test the orientation of the viewport (or the page box, for paged media).The orientation feature is specified as a keyword value chosen from the list below.
- portrait——The viewport is in a portrait orientation, i.e., the height is greater than or equal to the width.
- landscape——The viewport is in a landscape orientation, i.e., the width is greater than the height.
# CSS Media Queries
通过媒体查询的方式,我们可以通过以下方法来实现根据横竖屏不同的情况来适配样式:
- 内联样式
@media screen and (orientation: portrait) {
//竖屏
}
@media screen and (orientation: landscape) {
//横屏
}
2
3
4
5
6
- 外联样式
<!-- 竖屏 -->
<link rel="stylesheet" media="all and (orientation:portrait)" href="" />
<!-- 横屏 -->
<link rel="stylesheet" media="all and (orientation:landscape)" href="" />
2
3
4
# 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 函数 兼容性方面Can I Use - matchMeida,该 API 在移动端得到良好的支持,并无兼容性问题,pc 端支持 ie10+及现代浏览器。 那么,通过 window.matchMedia() 的方法,我们可以这样判断横竖屏:
var mql = window.matchMedia('(orientation: portrait)');
function onMatchMeidaChange(mql) {
if (mql.matches) {
// 竖屏
} else {
// 横屏
}
}
onMatchMeidaChange(mql);
mql.addListener(onMatchMeidaChange);
2
3
4
5
6
7
8
9
10
# window.innerHeight/window.innerWidth
在 CSS Media Queries 中,Orientation 属性有两个值:
portrait,指的是当 height 大于等于 width 的情况
landscape,指的是当 height 小于 width 的情况
所以,还有一种最为常见的方法是通过比较页面的宽高,当页面的高大于等于宽时则认为是竖屏,反之则为横屏。
function detectOrient() {
if (window.innerHeight >= window.innerWidth) {
// 竖屏
} else {
// 横屏
}
}
detectOrient();
window.addEventListener('resize', detectOrient);
2
3
4
5
6
7
8
9
# window.orientation
不推荐使用,因为该 API 已经在 web 标准中移除,有些浏览器可能任然支持 在 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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
在实际应用中,对于 iPhone 和大部分 Android 是没有 180 度的手机竖屏翻转的情况的,但是 iPad 是存在的。所以,简化下代码,我们可以绑定 orientationchange 事件来判断横竖屏:
function detectOrient() {
if (Math.abs(window.orientation) === 90) {
// 横屏
} else {
// 竖屏
}
}
detectOrient();
window.addEventListener('orientationchange', detectOrient);
2
3
4
5
6
7
8
9
# 影响判断的因素
# window.orientation 属性值的不一致
在 iOS 平台,对 window.orientation 属性值是无异议的,规范当中有明确规定每个值对应的情况。但是对于 Android 平台(如: Samsung Tab 2),就有不一致的特殊情况出现。
按照Compatibility Standard - 4.2 window.orientation API规范中的定义,0 值指的是 natural 、 default 的屏幕方向,所以如果生厂商对 natural 、 default 状态是用户应当手持设备方向为横屏,那么 0 值对应为 landscape 的横屏方向了。
针对这种不一致情况的出现,对于追求完美的开发者来说,通过 window.orientation 的方法来判断横竖屏则变得有点不可靠的。
而且在最新标准中 window.orientation 以被移除,真心不推荐使用。
# 软键盘的弹出
是否除了 window.orientation 的其它方法都是可靠的呢?
然而,实际上是事与愿违的。在 Android 下,如果页面中出现软键盘弹出的情况(存在有 Input 的元素)时,页面有时会因为软键盘的弹出而导致页面回缩,即页面的宽度(竖屏时)或者高度(横屏时)被改变。
无论是 CSS Media Queries 还是 window.matchMedia() 方法,还是根据 window.innerWidth 、window.innerHeight 的页面宽高比对方法来实现的横竖屏判断方法,都会因此受到影响,出现判断失误的情况( Samsung SCH-i699 机型,在竖屏时由于软键盘弹出导致页面高度小于宽度,被错误地判定为横屏)。
所以,在这样的情况下,这几种方式也变得不可靠。
但是可以设置一个定时器来延迟获取尺寸。
# 在 华为 P9 的微信(6.5.4)、华为荣耀的微信(6.5.7)和 Chrome 浏览器上,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 进行比较,从而判断出横竖屏。
# 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 的值不相同?“的原因所在。 因此,在这里也补充一点,在笔者提出的方法中,有个忘记跟大家说明的前提——页面设置了以下属性以保证页面的适配:
<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 其中一值相等,并且这也是本文提出的横竖屏检测方法的核心。
# 在微信内(其他移动浏览器也会),会多次触发 resize 事件。
笔者是通过绑定监听 resize 事件来响应执行横竖屏检测方法的,而在实际应用中确实出现了 resize 事件触发两次的情况。
虽然并没有影响到事件的判断结果,但是这也算个值得优化的点,而且问题也不大,我们只要通过函数去抖( Debounce Function ) 办法来进行简单的解决就好。
# 探讨最佳实现方式
本着核心的原则——具体情况具体解决来讨论。
如果你没有遇上以上两个问题所在,恭喜你!上面所提到的方法都可以被应用,选择你最为喜欢的方法就好。
但是如果想要避免以上两个问题所在,有没有更好的办法呢?
经过实际情况的研究,针对开发环境兼容的情况( iOS 与 Android 下的微信内置浏览器与原生浏览器)来说,屏幕分辨率是不会改变的,那么我们可以尝试比对页面宽高和屏幕分辨率来判断横竖屏。
需要注意的是,微信内置浏览器页面宽度不包括顶栏部分的,而 Android 和 iOS 的原生浏览器都是带有底栏或顶栏兼有的。
那么,我们可以确定为:
假如屏幕分辨率固定值为:screen.width 和 screen.height(需要注意,这里很重要的一点是:在移动端,屏幕翻转时,screen.width 和 screen.height 的值依然是不变的后面有补充修正,可以直接跳到下一个章节阅读)
- 若获取 当前页面的宽(document.documentElement.clientWidth),等于屏幕分辨率的宽(screen.width),则可认定当前属于竖屏。
- 若获取 当前页面的宽(document.documentElement.clientWidth),等于屏幕分辨率的高(screen.height),则可认定当前属于横屏。 如此,对应的代码为:
// 判断横竖屏
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();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
目前,W3C 引入Screen Orientation API,该标准能够帮助 Web 应用获得屏幕方向的状态,在状态改变时获得通知,并能够从应用程序中将屏幕状态锁定到特定状态。
但截止目前,该标准仍在 W3C 草案阶段。在移动端,它在 Android 和 iOS 平台上仍未得到支持,仅仅在 Chrome for Android 39 版本及以上才得到实现,所以对目前的开发来说意义不大。只能期待它能够尽快通过并得到广泛支持,这样的检测屏幕方向的问题就能够得到规范化的解决。