最近再改公司内部移动端 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) {
-
- }
复制代码 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |