ios 刘海屏,灵动岛的适配以及遇到一些坑点
最近再改公司内部移动端 ios 安全区域适配的问题,所谓知己知彼百战百胜,所以深入的学习了一下 安全区域适配有关的知识。
最近再改公司内部移动端 ios 安全区域适配的问题,所谓知己知彼百战百胜,所以深入的学习了一下 安全区域适配有关的知识
问题表象
-
头部标题区域被灵动岛遮挡,标题被遮挡,返回按钮点不到
-
底部被遮挡
原因
看一下官方对安全区域[1]的定义。
视图中未被导航栏、选项卡栏、工具栏或视图控制器可能提供的其他视图覆盖的区域。
看一下官方的图就能明白了,蓝色区域是安全区域,白色区域是可能覆盖安全区域的其他视图范围。也就是说,我们要做好适配,必须保证页面可视、可操作区域是在安全区域内。
第一次提出这个概念是 iOS11
,iPhoneX
机型发布的时候。从 iPhoneX
开始推出了刘海屏,从 iPhone 14 Pro
开始推出了灵动岛。后面的讨论,均以 iPhoneX
机型为例。
处理方案
其实处理方案还是很简单的,将 viewport-fit
指定为cover
,这样我们就可以使用 env()
和 constant()
这两个函数在css
中获取到安全区域的值了,获取到值之后处理对应的间距就可以了。
但是使用的过程中多多少少还是有一些坑点的,下面我来分享一些。
viewport-fit
iOS11
新增特性,苹果公司为了适配 iPhoneX
对现有 viewport meta
标签做了一个扩展,新增viewport-fit
属性,用于设置网页在可视窗口的布局方式,可设置三个值:
-
contain
:可视窗口完全包含网页内容(左图) -
cover
:网页内容完全覆盖可视窗口(右图) -
auto
:默认值,此值不影响初始布局视图端口,并且整个web
页面都是可查看的。
contain
和 cover
具体区别如下图:
注意:网页的默认值是
viewport-fit=contain
,需要适配iPhoneX
必须设置viewport-fit=cover
,这是适配的关键步骤。
<meta name="viewport" content="xxx(你们之前的viewport属性值),viewport-fit=cover" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
更详细说明,参考文档: viewport-fit-descriptor[2]
坑点 1
viewport-fit
从 contain
修改成 cover
之后,页面可能会出现白边~~(你要是问为啥,就说明你 viewport-fit 干啥用的没看懂 (ಥ_ಥ))~~,处理方案也很简单,可以给整个页面添加一个页面主题色的背景色,如下图。
env() 和 constant()
iOS11
新增特性,Webkit
的一个 CSS
函数,用于设定安全区域与边界的距离,有四个预定义的变量:
-
safe-area-inset-top
:安全区域距离顶部边界距离 -
safe-area-inset-bottom
:安全区域距离底部边界距离 -
safe-area-inset-left
:安全区域距离左边边界距离 -
safe-area-inset-right
:安全区域距离右边边界距离
有一点要注意,在 IOS11.2
系统以前,可以使用 constant()
函数,但是在 IOS11.2
系统以后,这个函数就被废弃了,被 env()
函数替代了。所以我们在做屏幕适配时,需要这样写:
.footer {
padding-bottom: constant(safe-area-inset-bottom); // 兼容 IOS 版本 < 11.2
padding-bottom: env(safe-area-inset-bottom); // 兼容 IOS 版本 >= 11.2
}
并且env()
和 constant()
需要同时存在,而且顺序不能换。
使用@supports 一点点的优化
如果我们想只有在需要适配的机型上才适配的时候,我们可以使用 css @supports
语法,检测到这两个变量存在的时候,@supports
内写的 css
样式才会被应用。
@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
.footer {
margin-bottom: constant(safe-area-inset-bottom);
margin-bottom: env(safe-area-inset-bottom);
}
}
如何优雅的在 js 中获取到这些变量
很多时候,我们会用 JS
做一些效果,比如页面的头部滚动到一定程度的时候需要固定在页面顶上。这个时候就不得不使用 JS
获取到安全区域的值,去适配这个定位的元素。那么现在的问题就变成了,怎么优雅的在 JS
中获取到 safe-area-inset-xxx
的值。
其实原理很简单,核心上就是 safe-area-inset-xxx
其实就是苹果为我们注入的 css
变量,我们可以自定义 css
变量,让后使用 JS
获取自定义的 css
变量。
看一段成熟的代码:
function insertSafeAreaCSSVarEl() {
const cssVarStyleElVer = 1;
const cssVarStyleElId = 'beisen_mobile_utils_safe_area_vars';
const existedEl = document.getElementById(cssVarStyleElId);
if (existedEl) {
const existedVer = parseInt(existedEl.dataset.version || '0');
if (existedVer >= cssVarStyleElVer) {
return;
}
existedEl.parentElement?.removeChild(existedEl);
}
const el = document.createElement('style');
el.setAttribute('id', cssVarStyleElId);
el.dataset.version = String(cssVarStyleElVer);
el.innerHTML = `
:root {
--yl-sat: constant(safe-area-inset-top);
--yl-sat: env(safe-area-inset-top);
--yl-sar: constant(safe-area-inset-right);
--yl-sar: env(safe-area-inset-right);
--yl-sab: constant(safe-area-inset-bottom);
--yl-sab: env(safe-area-inset-bottom);
--yl-sal: constant(safe-area-inset-left);
--yl-sal: env(safe-area-inset-left);
}`;
document.head.append(el);
}
function parseRootPropertyValue(key) {
let str = getComputedStyle(document.documentElement).getPropertyValue(key);
str = str.replace('px', '');
const n = parseInt(str) || 0;
return n;
}
* 获取SafeArea的尺寸。
*/
function getSafeAreaSize() {
insertSafeAreaCSSVarEl();
return {
top: parseRootPropertyValue('--yl-sat'),
right: parseRootPropertyValue('--yl-sar'),
bottom: parseRootPropertyValue('--yl-sab'),
left: parseRootPropertyValue('--yl-sal'),
};
}
核心点是 往 :root
中插入了一些自定义的 css
变量。
坑点 2
上面提到的 getSafeAreaSize
方法有点弊端,我们是主动的往 html
上插入了一个style
标签,经过测试,在插入标签的时候,env(safe-area-inset-xxx)
值是空的,等到下一帧渲染的时候才能拿到最新的值。就是说我们第一次获取值的时候是有一定的延迟。
这个问题有两个解决方案:
-
一个是 往
:root
中插入变量的过程放到html
页面中,我们请求回来的html
文件中就有这些变量了,那我们JS
中只需要获取就可以了,是在不行也可以放到index.css
中,初始化样式的时候可以往:root
或者body
中加自定义的css
变量。 -
第二个是我们要用之前,提前的执行一下
getSafeAreaSize()
方法。比如在组件的useLayoutEffect
这个hooks
中执行一下(我们是因为有历史原因,没有采用第一种方案,但实际上第一种方案的成本几乎没有)。
坑点 3
虽然说苹果会为我们注入 safe-area-inset-xxx
变量到浏览器中,但是这玩意完全是一个黑盒,经过测试发现他是在需要注入的时候才会注入。
那什么叫做 需要注入的时候才会注入呢?
写下如下代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, minimum-scale=1, maximum-scale=1.0, user-scalable=0" />
<style>
#test123 {
background: red;
width: 100px;
height: 100px;
}
</style>
<script>
function getSafeAreaSize() {
}
getSafeAreaSize();
</script>
</head>
<body>
<div id="test123"></div>
<script>
const el = document.getElementById('test123');
setTimeout(() => {
el.innerText = JSON.stringify(getSafeAreaSize());
}, 20);
</script>
</body>
</html>
OK,上面的代码起个本地服务(可以用 Live Server 插件),用手机访问(同一个 WiFi,用 ip+端口访问)一下。
Safari 浏览器的表象如下:
我们发现明明是 14Pro 的机型,但是 top 和 bottom 的值是 0。
再看一下微信内置浏览器打开是什么样的。
我们发现 bottom
有值,但是 top
却没有值。
综上可以看出,我们的页面存在的区域被操作栏或者底部小黑条遮挡了的时候,safe-area-inset-xxx
才会有值,也就是我上面说的 需要注入的时候才会注入。
坑点 4
还是微信内置浏览器,他有一个特性,就是 A 页面跳转到 B 页面的时候,B 页面下面会自带一个底部操作区。如图
出现了底部操作区的时候,我们的页面就不需要再做适配了
适配的伪代码如下
let safeBottom = 0;
if (isWechat && !isWechatWork) {
safeBottom = window.history.length > 1 ? 0 : 'env(safe-area-inset-bottom)';
}
处理方案 2-不推荐
在学习的过程中发现我们的系统里面还有如下的实现,使用 @media
检查当前屏幕的分辨率信息,来判断对应的机型,然后做相关的适配。只做一种学习了解,不推荐这个方案实现。
@media only screen and (min-device-width: 375px) and (max-device-height: 812px) and (-webkit-device-pixel-ratio: 3) {
}
@media only screen and (min-device-width: 390px) and (max-device-height: 844px) and (-webkit-device-pixel-ratio: 3) {
}
@media only screen and (min-device-width: 414px) and (max-device-height: 896px) and (-webkit-device-pixel-ratio: 2) {
}
@media only screen and (min-device-width: 414px) and (max-device-height: 896px) and (-webkit-device-pixel-ratio: 3) {
}
@media only screen and (min-device-width: 428px) and (max-device-height: 926px) and (-webkit-device-pixel-ratio: 3) {
}
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)