了解 WebView 中的窗口边衬区

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

功能兼容性

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

Milestone 添加了功能 范围
M136 通过 CSS safe-area-insets 实现 displayCutout()systemBars() 支持。 仅限全屏 WebView。
M139 通过调整可视视口大小来支持 ime()(输入法编辑器,即键盘)。 所有 WebView。
M144 displayCutout()systemBars() 支持。 所有 WebView(无论是否处于全屏状态)。

如需了解详情,请参阅 WindowInsetsCompat

核心机制

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

  • 安全区域(displayCutoutsystemBars:WebView 通过 CSS safe-area-inset-* 变量将这些维度转发到 Web 内容。这样一来,开发者便可防止自己的互动元素(例如导航栏)被凹口或状态栏遮挡。

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

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

边界和重叠逻辑

仅当系统界面元素(栏、刘海屏或键盘)直接与 WebView 的屏幕边界重叠时,WebView 才应接收非零边衬区值。如果 WebView 不与这些界面元素重叠(例如,如果 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;
});

管理调整大小事件

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

  1. 用户将焦点置于输入元素上。
  2. 键盘显示,触发调整大小事件。
  3. 网站的代码会清除焦点以响应调整大小操作。
  4. 键盘因失去焦点而隐藏。

为缓解此行为,请检查网页端监听器,确保视口变化不会意外触发 blur() JavaScript 函数或焦点清除行为。

实现边衬区处理

WebView 的默认设置可自动适用于大多数应用。不过,如果您的应用使用自定义布局(例如,如果您添加了自己的内边距来考虑状态栏或键盘),则可以使用以下方法来改进网页内容和原生界面之间的协同工作方式。如果您的原生界面基于 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. 拦截边衬区:在 WebView 子类中使用 setOnApplyWindowInsetsListener 或替换 onApplyWindowInsets

  2. Clear insets: 从头开始返回一组已消耗的边衬区(例如 WindowInsetsCompat.CONSUMED)。此操作会阻止插屏通知完全传播到 WebView,从而有效地停用现代视口调整大小功能,并强制 WebView 保留其初始可视视口大小。