了解 WebView 中的窗口边衬区

WebView 使用两个视口来管理内容对齐:布局视口(页面大小)和视觉视口(用户实际看到的页面部分)。布局视口通常是静态的,而视觉视口会随着用户缩放、滚动或系统界面元素(例如软件键盘)的出现而动态变化。

功能兼容性

WebView 对窗口边衬区的支持随着时间的推移而不断演变,以使 Web 内容行为与原生 Android 应用预期保持一致:

里程碑 已添加功能 范围
M136 通过 CSS safe-area-insets 支持 displayCutout() 和 systemBars()。 仅限全屏 WebView。
M139 通过调整视觉视口大小来支持 ime()(输入法,即键盘)。 所有 WebView。
M144 displayCutout() 和 systemBars() 支持。 所有 WebView(无论是否处于全屏状态)。

如需了解详情,请参阅 WindowInsetsCompat。

核心机制

WebView 通过以下两种主要机制处理边衬区:

  • 安全区域(displayCutout、systemBars):WebView 会通过 CSS safe-area-inset-* 变量将这些尺寸转发到 Web 内容。这样,开发者就可以防止自己的互动元素(例如导航栏)被刘海屏或状态栏遮挡。

  • 使用输入法 (IME) 调整视觉视口大小:从 M139 开始,输入法 (IME) 会直接调整视觉视口的大小。此调整大小机制也基于 WebView 与窗口的交集。例如,在 Android 多任务模式下,如果 WebView 的底部超出窗口底部 200dp,则视觉视口会比 WebView 的尺寸小 200dp。这种视觉视口大小调整(针对 IME 和 WebView 窗口交集)仅应用于 WebView 的底部。此机制不支持针对左侧、右侧或顶部重叠调整大小。这意味着,出现在这些边缘的停靠键盘不会触发视觉视口大小调整。

以前,视觉视口保持固定,通常会将输入字段隐藏在键盘后面。通过调整视口大小,默认情况下,网页的可见部分会变为可滚动,从而确保用户可以访问被遮挡的内容。

边界和重叠逻辑

仅当系统界面元素(系统栏、刘海屏或键盘)与 WebView 的屏幕边界直接重叠时,WebView 才应收到非零边衬区值。如果 WebView 未与这些界面元素重叠(例如,如果 WebView 位于屏幕中央且未触及系统栏),则应将这些边衬区设为零。

如需替换此默认逻辑,并为 Web 内容提供完整的系统尺寸(无论是否重叠),请使用 setOnApplyWindowInsetsListener 方法,并从监听器返回原始、未修改的 windowInsets 对象。提供完整的系统尺寸有助于确保设计一致性,因为无论 WebView 当前位于何处,Web 内容都能与设备硬件保持一致。这可确保 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;
});

管理调整大小事件

由于键盘可见性现在会触发视觉视口大小调整,因此 Web 代码可能会看到更频繁的大小调整事件。开发者必须确保其代码不会通过清除元素焦点来响应这些调整大小事件。这样做会造成焦点丢失和键盘关闭的循环,从而阻止用户输入:

  1. 用户聚焦于某个输入元素。
  2. 键盘出现,触发大小调整事件。
  3. 网站的代码会清除焦点以响应大小调整。
  4. 键盘因失去焦点而隐藏。

为缓解此行为,请检查 Web 端监听器,确保视口更改不会无意中触发 blur() JavaScript 函数或焦点清除行为。

实现边衬区处理

WebView 的默认设置适用于大多数应用。不过,如果您的应用使用自定义布局(例如,您添加了自己的内边距来考虑状态栏或键盘),则可以使用以下方法来改进 Web 内容和原生界面的协同工作方式。如果您的原生界面根据 WindowInsets 为容器应用内边距,则必须在这些边衬区到达 WebView 之前正确管理它们,以避免双重内边距。

双重内边距是指原生布局和 Web 内容应用相同的边衬区尺寸,从而导致出现多余的间距。例如,假设手机的状态栏高度为 40 像素。原生视图和 WebView 都会看到 40 像素的边衬区。两者都会添加 40 像素的内边距,导致用户在顶部看到 80 像素的间隙。

归零方法

为防止重复内边距,您必须确保在原生视图使用边衬区尺寸作为内边距后,使用 Insets.NONE 在新的 WindowInsets 对象上将该尺寸重置为零,然后再将修改后的对象沿视图层次结构向下传递到 WebView。

对父视图应用内边距时,通常应使用归零方法,即设置 Insets.NONE 而不是 WindowInsetsCompat.CONSUMED。在某些情况下,返回 WindowInsetsCompat.CONSUMED 可能有效。 不过,如果应用的处理器更改了边衬区或添加了自己的内边距,则可能会遇到问题。归零方法没有这些限制。

通过将边衬区设为零来避免幽灵内边距

如果应用之前传递了未使用的边衬区,而您使用了这些边衬区,或者边衬区发生了变化(例如键盘隐藏),那么使用这些边衬区会阻止 WebView 收到必要的更新通知。这可能会导致 WebView 保留先前状态的虚影内边距(例如,在键盘隐藏后保留键盘内边距)。

以下示例展示了应用与 WebView 之间的互动中断:

  1. 初始状态:应用最初会将未使用的边衬区(例如 displayCutout() 或 systemBars())传递给 WebView,WebView 会在内部对 Web 内容应用内边距。
  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. 拦截 Inset:使用 setOnApplyWindowInsetsListener 或在 WebView 子类中替换 onApplyWindowInsets。

  2. 清除 Insets:从一开始就返回一组已使用的 Insets(例如 WindowInsetsCompat.CONSUMED)。此操作会阻止边衬区通知传播到 WebView,从而有效停用现代视口大小调整功能,并强制 WebView 保留其初始视觉视口大小。