在一些Android设备中,可以识别safe-area-inset-top属性,但又没有高度,这种问题该怎么解决呢?

从 iPhone X 开始出现了刘海和底部的黑条的区域,而 Android 系统通常也会模仿 iPhone 的一些设计,然后就是现在越来越多地新机型有了安全区域的概念。

若完全不考虑这些,可能就会出现类似这样的情况:

android 区域 安全 屏幕 手机安全区域设置_android 区域 安全 屏幕

因此我们需要对这些区域做些特殊地处理。

1. 适配 iOS 中的安全区域

在大部分机型,尤其是 iOS 设备中,适配安全区域还是比较简单的,主要是 3 个步骤。

android 区域 安全 屏幕 手机安全区域设置_android 区域 安全 屏幕_02

1.1 设置网页在可视窗口的布局方式

新增 viweport-fit 属性,使得页面内容完全覆盖整个窗口:

<meta name="viewport" content="width=device-width, viewport-fit=cover">

只有设置了 viewport-fit=cover,才能使用 env()。

1.2 限定安全区域

iOS11 新增特性,Webkit 的一个 CSS 函数,用于设定安全区域与边界的距离,有四个预定义的变量:

  • safe-area-inset-left:安全区域距离左边边界的距离
  • safe-area-inset-right:安全区域距离右边边界的距离
  • safe-area-inset-top:安全区域距离顶部边界的距离
  • safe-area-inset-bottom:安全区域距离底部边界的距离

这里我们只需要关注safe-area-inset-bottom这个变量,因为它对应的就是小黑条的高度。

注意:当 viewport-fit=contain 时 env() 是不起作用的,必须要配合 viewport-fit=cover 使用。对于不支持 env() 的浏览器,浏览器将会忽略它。

> The env() function shipped in iOS 11 with the name constant(). Beginning with Safari Technology Preview 41 and the iOS 11.2 beta, constant() has been removed and replaced with env(). You can use the CSS fallback mechanism to support both versions, if necessary, but should prefer env() going forward.

这就意味着,之前使用的 constant() 在 iOS11.2 之后就不能使用的,但我们还是需要做向后兼容,像这样:

body {
  padding-bottom: constant(safe-area-inset-bottom); /* 兼容 iOS < 11.2 */
  padding-bottom: env(safe-area-inset-bottom); /* 兼容 iOS >= 11.2 */
}

注意:env() 跟 constant() 需要同时存在,而且顺序不能换。

android 区域 安全 屏幕 手机安全区域设置_iOS_03

在竖屏的情况下,若要限定顶部的刘海区域,则要使用safe-area-inset-top;若要限定底部的区域,则要使用safe-area-inset-bottom

若需要进行计算,则可以使用calc()函数:

body {
  padding-bottom: calc(12px + constant(safe-area-inset-bottom));
  padding-bottom: calc(12px + env(safe-area-inset-bottom));
}

2. 部分奇特的 Android 手机

很多 Android 手机也会按照 iOS 的标准来实现安全区域,因此上面的属性在大部分 Android 手机上也能正常使用。

但是,我们在测试的过程中发现,有几个奇特的手机,会出现下图的状况:

android 区域 安全 屏幕 手机安全区域设置_Android_04

通过 Chrome 查看样式,发现他会识别 safe-area-inset-top 等预定义变量,但又将其解析为 0。

这就导致即使我们设置了兜底的数据,也无法使用。例如我们在不支持安全区域属性时,使用兜底的 padding-top: 25PX 样式(大写的 PX 是为了不被插件转义成 vw 或 rem),但上述的 Android 设备中,兜底样式也不会生效:

body {
  /* prettier-ignore */
  padding-top: 25PX;
  padding-top: constant(safe-area-inset-top);
  padding-top: env(safe-area-inset-top);
}

那么如何解决这个问题呢?

android 区域 安全 屏幕 手机安全区域设置_iOS_05

3. 解决方案

这里我们就要借助 js 来实现了。

首先我们向页面中插入一个看不见的 div,将 div 的高度设置为安全距离的高度,然后再通过 js 获取其高度,若高度为 0,则说明没有生效。

let status = 0; // 0:还没数据,-1:不支持,1:支持

/**
 * 判断当前设置是否支持constant(safe-area-inset-top)或env(safe-area-inset-top);
 * 部分Android设备,可以认识safa-area-inset-top,但会将其识别为0
 * @returns {boolean} 当前设备是否支持安全距离
 */
const supportSafeArea = (): boolean => {
  if (status !== 0) {
    // 缓存数据,只向 body 插入一次 dom 即可
    return status === 1;
  }
  const div = document.createElement('div');
  const id = 'test-check-safe-area';
  const styles = [
    'position: fixed',
    'z-index: -1',
    'height: constant(safe-area-inset-top)',
    'height: env(safe-area-inset-top)',
  ];
  div.style.cssText = styles.join(';');
  div.id = id;
  document.body.appendChild(div);
  const areaDiv = document.getElementById(id);
  if (areaDiv) {
    status = areaDiv.offsetHeight > 0 ? 1 : -1; // 该 div 的高度是否为 0
    areaDiv.parentNode?.removeChild(areaDiv);
  }
  return status === 1;
};

那么在已经设置了安全区域属性的地方,都需要额外执行下 supportSafeArea()方法:

const SignTaskDetail = () => {
  const [safaArea, setSafeArea] = useState(true); // 当前页面是否支持 safe-area-inset-top

  useEffect(() => {
    setSafeArea(supportSafeArea());
  }, []);

  return (
    <div classname="{classNames('task-page'," {="" 'task-page-not-safearea':="" !safaarea,="" 不支持时,需要额外设置属性="" })}=""></div>
  );
};

android 区域 安全 屏幕 手机安全区域设置_android 区域 安全 屏幕_06

4. 总结

设备兼容性一直我们前端在解决的问题,无论是在 PC 端还是在移动端,浏览器的多样性和编程语言的发展,必然需要解决这些问题。