如何解決移動端的安全區域為0的問題

蚊子部落格發表於2021-05-20

從 iPhone X 開始出現了劉海和底部的黑條的區域,而 Android 系統通常也會模仿 iPhone 的一些設計,然後就是現在越來越多地新機型有了安全區域的概念。

若完全不考慮這些,可能就會出現類似這樣的情況:

內容被遮擋

因此我們需要對這些區域做些特殊地處理。

1. 適配 iOS 中的安全區域

在大部分機型,尤其是 iOS 裝置中,適配安全區域還是比較簡單的,主要是 3 個步驟。

圍觀中

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 &lt; 11.2 */
  padding-bottom: env(safe-area-inset-bottom); /* 相容 iOS &gt;= 11.2 */
}

注意:env() 跟 constant() 需要同時存在,而且順序不能換。

在豎屏的情況下,若要限定頂部的劉海區域,則要使用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不支援安全區域

通過 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);
}

那麼如何解決這個問題呢?

我養你

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 =&gt; {
  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 &gt; 0 ? 1 : -1; // 該 div 的高度是否為 0
    areaDiv.parentNode?.removeChild(areaDiv);
  }
  return status === 1;
};

那麼在已經設定了安全區域屬性的地方,都需要額外執行下 supportSafeArea()方法:

const SignTaskDetail = () =&gt; {
  const [safaArea, setSafeArea] = useState(true); // 當前頁面是否支援 safe-area-inset-top

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

  return (
    <div classname="{classNames('task-page'," {="" 'task-page-not-safearea':="" !safaarea,="" 不支援時,需要額外設定屬性="" })}=""></div>
  );
};

4. 總結

裝置相容性一直我們前端在解決的問題,無論是在 PC 端還是在移動端,瀏覽器的多樣性和程式語言的發展,必然需要解決這些問題。

也歡迎您關注我的公眾號:“前端小茶館”。

前端小茶館公眾號

相關文章