最近再改公司内部移动端 ios 安全区域适配的问题,所谓知己知彼百战百胜,所以深入的学习了一下 安全区域适配有关的知识

问题表象

  • 头部标题区域被灵动岛遮挡,标题被遮挡,返回按钮点不到

  • 底部被遮挡

原因

看一下官方对安全区域[1]的定义。

视图中未被导航栏、选项卡栏、工具栏或视图控制器可能提供的其他视图覆盖的区域。

看一下官方的图就能明白了,蓝色区域是安全区域,白色区域是可能覆盖安全区域的其他视图范围。也就是说,我们要做好适配,必须保证页面可视、可操作区域是在安全区域内。

第一次提出这个概念是 iOS11iPhoneX 机型发布的时候。从 iPhoneX 开始推出了刘海屏,从 iPhone 14 Pro 开始推出了灵动岛。后面的讨论,均以 iPhoneX 机型为例。

处理方案

其实处理方案还是很简单的,将 viewport-fit 指定为cover,这样我们就可以使用 env()constant()这两个函数在css中获取到安全区域的值了,获取到值之后处理对应的间距就可以了。

但是使用的过程中多多少少还是有一些坑点的,下面我来分享一些。

viewport-fit

iOS11 新增特性,苹果公司为了适配 iPhoneX 对现有 viewport meta 标签做了一个扩展,新增viewport-fit属性,用于设置网页在可视窗口的布局方式,可设置三个值:

  • contain:可视窗口完全包含网页内容(左图)

  • cover:网页内容完全覆盖可视窗口(右图)

  • auto:默认值,此值不影响初始布局视图端口,并且整个 web 页面都是可查看的。

containcover 具体区别如下图:

注意:网页的默认值是 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-fitcontain 修改成 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) {
  
}

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐