WebView 使用两个视口管理内容对齐方式:布局视口 (页面大小)和视觉视口(用户 实际看到的页面部分)。布局视口通常是静态的,而视觉视口会在用户缩放、滚动或系统界面元素(例如软件键盘)出现时动态变化。
功能兼容性
WebView 对窗口插页的支持随着时间的推移而不断发展,以使 Web 内容行为与原生 Android 应用的预期保持一致:
| 里程碑 | 添加的功能 | 范围 |
|---|---|---|
| M136 | 通过 CSS 安全区域插页支持 displayCutout() 和 systemBars()。 |
仅限全屏 WebView。 |
| M139 | 通过调整视觉视口大小支持 ime()(输入法,即键盘)。 |
所有 WebView。 |
| M144 | 支持 displayCutout() 和 systemBars()。 |
所有 WebView(无论是否处于全屏状态)。 |
如需了解详情,请参阅 WindowInsetsCompat。
核心机制
WebView 通过两种主要机制处理插页:
安全区域(
displayCutout、systemBars): WebView 通过 CSS safe-area-inset-* 变量将这些尺寸转发给 Web 内容。这样,开发者就可以防止自己的互动元素(例如导航栏)被凹口或状态栏遮挡。使用输入法 (IME) 调整视觉视口大小: 从 M139 开始,输入法 (IME) 会直接调整视觉视口的大小。这种调整机制还基于 WebView-Window 交集。例如,在 Android 多任务模式下,如果 WebView 的底部延伸到窗口底部下方 200dp,则视觉视口比 WebView 的大小小 200dp。这种视觉视口调整(对于 IME 和 WebView-Window 交集)仅适用于 WebView 的底部。此机制不支持调整左侧、右侧或顶部重叠的大小。这意味着,出现在这些边缘的停靠键盘不会触发视觉视口调整。
以前,视觉视口保持固定,通常会将输入字段隐藏在键盘后面。通过调整视口大小,页面的可见部分默认变为可滚动,确保用户可以访问被遮挡的内容。
边界和重叠逻辑
只有当系统界面元素(栏、显示屏凹口或键盘)与 WebView 的屏幕边界直接重叠时,WebView 才应收到非零插页值。如果 WebView 不与这些界面元素重叠(例如,如果 WebView 位于屏幕中央且不触及系统栏),则应将这些边衬区作为零接收。
如需替换此默认逻辑并向 Web 内容提供完整的系统尺寸(无论是否重叠),请使用 setOnApplyWindowInsetsListener 方法,并从监听器返回原始的、未修改的 windowInsets 对象。提供完整的系统尺寸有助于确保设计一致性,方法是使 Web 内容能够与设备硬件对齐,而无论 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 代码可能会看到更频繁的调整大小事件。开发者必须确保其代码不会通过清除元素焦点来响应这些调整大小事件。这样做会创建一个焦点丢失和键盘关闭的循环,从而阻止用户输入:
- 用户将焦点放在输入元素上。
- 键盘出现,触发调整大小事件。
- 网站的代码会清除焦点以响应调整大小。
- 键盘会因焦点丢失而隐藏。
如需缓解此行为,请查看 Web 端监听器,确保视口更改不会意外触发 blur() JavaScript 函数或焦点清除行为。
实现边衬区处理
WebView 的默认设置可自动适用于大多数应用。不过,如果您的应用使用自定义布局(例如,如果您添加自己的内边距以考虑状态栏或键盘),则可以使用以下方法来改进 Web 内容和原生界面的协同工作方式。如果您的原生界面根据 WindowInsets 向容器应用内边距
,则必须在这些插页
到达 WebView 之前正确管理它们,以避免双重内边距。
双重内边距是指原生布局和 Web 内容应用相同的插页尺寸,从而导致冗余间距。例如,假设有一部手机的状态栏为 40 像素。原生视图和 WebView 都会看到 40 像素的插页。两者都添加了 40 像素的内边距,导致用户在顶部看到 80 像素的间距。
归零方法
如需防止双重内边距,您必须确保在原生视图使用边衬区尺寸作为内边距后,您可以使用新 WindowInsets 对象上的 Insets.NONE 将该尺寸重置为零,然后再将修改后的对象向下传递到视图层次结构中的 WebView。
在向父视图应用内边距时,您通常应使用归零方法,即设置 Insets.NONE 而不是 WindowInsetsCompat.CONSUMED。在某些情况下,返回 WindowInsetsCompat.CONSUMED 可能会有效。
不过,如果应用的处理程序更改插页或添加自己的内边距,则可能会遇到问题。归零方法没有这些限制。
通过将插页归零来避免幽灵内边距
如果应用之前传递了未使用的插页,而您使用了这些插页,或者插页发生了变化(例如键盘隐藏),则使用这些插页会阻止 WebView 接收必要的更新通知。这可能会导致 WebView 保留之前状态的幽灵内边距(例如,在键盘隐藏后保留键盘内边距)。
以下示例展示了应用和 WebView 之间的互动中断:
- 初始状态: 应用最初将未使用的插页(例如
displayCutout()或systemBars())传递给 WebView,后者会在内部向 Web 内容应用内边距。 - 状态更改和错误: 如果应用更改状态(例如键盘隐藏),并且应用选择通过返回
WindowInsetsCompat.CONSUMED来处理生成的插页。 - 通知被屏蔽: 使用插页会阻止 Android 系统将必要的更新通知向下发送到视图层次结构中的 WebView。
- 幽灵内边距: 由于 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();
});
如何停用
如需停用这些新式行为并返回到旧版视口处理,请执行以下操作:
拦截插页: 使用
setOnApplyWindowInsetsListener或在onApplyWindowInsets子类中替换WebView。清除插页: 从一开始就返回一组已使用的插页(例如
WindowInsetsCompat.CONSUMED)。此操作会阻止边衬区通知完全传播到 WebView,从而有效地停用新式视口调整大小,并强制 WebView 保留其初始视觉视口大小。