瞭解 WebView 中的視窗插邊

WebView 會使用兩個可視區域管理內容對齊方式:版面配置可視區域 (網頁大小) 和視覺可視區域 (使用者實際看到的網頁部分)。版面配置檢視區塊通常是靜態的,但當使用者縮放、捲動或系統 UI 元素 (例如軟體鍵盤) 出現時,視覺檢視區塊會動態變更。

功能相容性

隨著時間演進,WebView 對視窗插邊的支援也隨之發展,以符合原生 Android 應用程式的預期,並與網頁內容行為保持一致:

Milestone 已新增功能 範圍
M136 displayCutout()systemBars() 支援,方法是透過 CSS safe-area-insets。 僅限全螢幕 WebView。
M139 ime() (輸入法編輯器,即鍵盤) 支援,方法是調整可視區域大小。 所有 WebView。
M144 displayCutout()systemBars() 支援。 所有 WebView (不論全螢幕狀態為何)。

詳情請參閱「WindowInsetsCompat」。

核心機制

WebView 會透過兩種主要機制處理插邊:

  • 安全區域 (displayCutoutsystemBars):WebView 會透過 CSS safe-area-inset-* 變數,將這些維度轉送至網頁內容。這樣一來,開發人員就能避免自己的互動式元素 (例如導覽列) 遭到凹口或狀態列遮蔽。

  • 使用輸入法編輯器 (IME) 調整視覺可視區域大小:從 M139 版開始,輸入法編輯器 (IME) 會直接調整視覺可視區域大小。這項大小調整機制也是以 WebView-Window 交集為依據。舉例來說,在 Android 多工模式中,如果 WebView 的底部超出視窗底部 200 dp,視覺檢視區塊就會比 WebView 的大小小 200 dp。這項視覺檢視區塊大小調整 (適用於 IME 和 WebView 視窗交集) 只會套用至 WebView 底部。這個機制不支援調整左側、右側或頂端重疊部分的大小。也就是說,出現在這些邊緣的固定鍵盤不會觸發可視區域大小調整。

先前,視覺可視區域會保持固定,導致輸入欄位經常遭到鍵盤遮住。調整可視區域大小後,網頁可見部分預設會變成可捲動,確保使用者能看到遭遮蔽的內容。

界線和重疊邏輯

只有在系統 UI 元素 (資訊列、螢幕凹口或鍵盤) 直接與 WebView 的螢幕界線重疊時,WebView 才應收到非零的插邊值。如果 WebView 與這些 UI 元素不重疊 (例如 WebView 位於畫面中央,且未觸及系統資訊列),則應會收到零值插邊。

如要覆寫這項預設邏輯,並提供網頁內容完整的系統尺寸 (無論是否重疊),請使用 setOnApplyWindowInsetsListener 方法,並從監聽器傳回原始、未修改的 windowInsets 物件。提供完整的系統尺寸,可讓網頁內容與裝置硬體對齊,無論 WebView 目前的位置為何,都能確保設計一致性。這樣一來,WebView 在移動或擴展至觸碰螢幕邊緣時,就能確保轉場效果順暢。

Kotlin

ViewCompat.setOnApplyWindowInsetsListener(myWebView) { _, windowInsets ->
    // By returning the original windowInsets object, we override the default
    // behavior that zeroes out system insets (like system bars or display
    // cutouts) when they don't directly overlap the WebView's screen bounds.
    windowInsets
}

Java

ViewCompat.setOnApplyWindowInsetsListener(myWebView, (v, windowInsets) -> {
  // By returning the original windowInsets object, we override the default
  // behavior that zeroes out system insets (like system bars or display
  // cutouts) when they don't directly overlap the WebView's screen bounds.
  return windowInsets;
});

管理大小調整事件

由於鍵盤顯示時會觸發視覺可視區域大小調整,網頁程式碼可能會更常看到大小調整事件。開發人員必須確保程式碼不會因清除元素焦點而對這些大小調整事件做出反應。這麼做會造成焦點遺失和鍵盤關閉的迴圈,導致使用者無法輸入內容:

  1. 使用者著重在輸入元素。
  2. 鍵盤隨即顯示,並觸發大小調整事件。
  3. 網站的程式碼會清除焦點,以回應大小調整。
  4. 鍵盤會因焦點遺失而隱藏。

如要避免這種情況,請檢查網頁端監聽器,確保視埠變更不會意外觸發 blur() JavaScript 函式或焦點清除行為。

實作插邊處理

WebView 的預設設定會自動套用至大多數應用程式。不過,如果應用程式使用自訂版面配置 (例如,您新增自己的邊框間距來考量狀態列或鍵盤),可以採用下列方法,改善網頁內容和原生 UI 的協同運作方式。如果原生 UI 會根據 WindowInsets 對容器套用邊框間距,您必須在這些插邊到達 WebView 前正確管理,以免出現雙重邊框間距。

如果原生版面配置和網頁內容套用相同的插邊尺寸,就會出現雙倍邊框間隔,導致間隔多餘。舉例來說,假設手機的狀態列為 40 像素,原生檢視區塊和 WebView 都會看到 40 像素的插邊。兩者都會新增 40 像素的邊框間距,因此使用者會在頂端看到 80 像素的間隙。

「歸零」方法

為避免雙重邊框間距,請務必在原生檢視區塊使用插邊維度做為邊框間距後,使用新 WindowInsets 物件上的 Insets.NONE 將該維度重設為零,再將修改後的物件傳遞至 WebView 的檢視區塊階層。

將邊框間距套用至父項檢視區塊時,一般應使用歸零方法,也就是設定 Insets.NONE,而非 WindowInsetsCompat.CONSUMED。在某些情況下,WindowInsetsCompat.CONSUMED可能會有效。 不過,如果應用程式的處理常式變更插邊或新增自己的邊框間距,就可能會發生問題。歸零做法則沒有這些限制。

將插邊歸零,避免出現空白邊框

如果應用程式先前已傳遞未使用的插邊,或插邊有所變更 (例如鍵盤隱藏),您在取用插邊時,系統就不會通知 WebView 進行必要更新。這可能會導致 WebView 保留先前狀態的虛影邊框間距 (例如在鍵盤隱藏後,仍保留鍵盤邊框間距)。

以下範例顯示應用程式與 WebView 之間的互動中斷:

  1. 初始狀態:應用程式一開始會將未使用的插邊 (例如 displayCutout()systemBars()) 傳遞至 WebView,後者會在內部將邊框間距套用至網頁內容。
  2. 狀態變更和錯誤:如果應用程式變更狀態 (例如鍵盤隱藏),且應用程式選擇透過傳回 WindowInsetsCompat.CONSUMED 來處理產生的插邊。
  3. 通知遭到封鎖:如果取用插邊,Android 系統就無法將必要的更新通知傳送至 WebView 的檢視區塊階層。
  4. 幽靈邊框間距:由於 WebView 未收到更新,因此會保留先前狀態的邊框間距,導致出現幽靈邊框間距 (例如,在鍵盤隱藏後仍保留鍵盤邊框間距)。

請改用 WindowInsetsCompat.Builder,在將物件傳遞至子項檢視畫面之前,將處理的型別設為零。這會通知 WebView,啟用通知繼續向下傳送至檢視區塊階層時,已將這些特定插邊納入考量。

Kotlin

ViewCompat.setOnApplyWindowInsetsListener(rootView) { view, windowInsets ->
    // 1. Identify the inset types you want to handle natively
    val types = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()

    // 2. Extract the dimensions and apply them as padding to the native container
    val insets = windowInsets.getInsets(types)
    view.setPadding(insets.left, insets.top, insets.right, insets.bottom)

    // 3. Return a new WindowInsets object with the handled types set to NONE (zeroed).
    // This informs the WebView that these areas are already padded, preventing
    // double-padding while still allowing the WebView to update its internal state.
    WindowInsetsCompat.Builder(windowInsets)
        .setInsets(types, Insets.NONE)
        .build()
}

Java

ViewCompat.setOnApplyWindowInsetsListener(rootView, (view, windowInsets) -> {
  // 1. Identify the inset types you want to handle natively
  int types = WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout();

  // 2. Extract the dimensions and apply them as padding to the native container
  Insets insets = windowInsets.getInsets(types);
  rootView.setPadding(insets.left, insets.top, insets.right, insets.bottom);

  // 3. Return a new Insets object with the handled types set to NONE (zeroed).
  // This informs the WebView that these areas are already padded, preventing
  // double-padding while still allowing the WebView to update its internal
  // state.
  return new WindowInsetsCompat.Builder(windowInsets)
    .setInsets(types, Insets.NONE)
    .build();
});

如何退出計劃

如要停用這些新版行為並返回舊版檢視區塊處理方式,請按照下列步驟操作:

  1. 截斷插邊:使用 setOnApplyWindowInsetsListener 或在 WebView 子類別中覆寫 onApplyWindowInsets

  2. 清除插邊:從頭傳回已耗用的插邊集 (例如 WindowInsetsCompat.CONSUMED)。這項動作可防止插邊通知完全傳播至 WebView,有效停用新式可視區域大小調整功能,並強制 WebView 保留初始可視區域大小。