Comprende las inserciones de ventana en WebView

WebView administra la alineación del contenido con dos viewports: la viewport de diseño (el tamaño de la página) y la viewport visual (la parte de la página que el usuario ve en realidad). Si bien el viewport de diseño suele ser estático, el viewport visual cambia de forma dinámica cuando los usuarios acercan o alejan la imagen, se desplazan o cuando aparecen elementos de la IU del sistema (como el teclado de software).

Compatibilidad de funciones

La compatibilidad de WebView con las inserciones de ventanas evolucionó con el tiempo para alinear el comportamiento del contenido web con las expectativas de las apps nativas para Android:

Meta Se agregó la función Alcance
M136 displayCutout() y systemBars() admiten a través de CSS safe-area-insets. Solo WebViews en pantalla completa.
M139 Compatibilidad con ime() (editor de método de entrada, que es un teclado) a través del cambio de tamaño visual del viewport Todos los objetos WebView.
M144 Compatibilidad con displayCutout() y systemBars() Todos los objetos WebView (independientemente del estado de pantalla completa).

Para obtener más información, consulta WindowInsetsCompat.

Mecánicas principales

WebView controla las inserciones a través de dos mecanismos principales:

  • Áreas seguras (displayCutout, systemBars): WebView reenvía estas dimensiones al contenido web a través de las variables CSS safe-area-inset-*. Esto permite que los desarrolladores eviten que sus propios elementos interactivos (como las barras de navegación) se vean oscurecidos por las muescas o las barras de estado.

  • Ajuste de tamaño visual del viewport con el editor de método de entrada (IME): A partir de M139, el editor de método de entrada (IME) ajusta directamente el tamaño del viewport visual. Este mecanismo de cambio de tamaño también se basa en la intersección entre WebView y Window. Por ejemplo, en el modo multitarea de Android, si la parte inferior de un WebView se extiende 200 dp por debajo de la parte inferior de la ventana, la ventana gráfica visual es 200 dp más pequeña que el tamaño del WebView. Este cambio de tamaño visual del viewport (tanto para la intersección de IME como de la ventana de WebView) solo se aplica a la parte inferior de WebView. Este mecanismo no admite el cambio de tamaño para la superposición izquierda, derecha o superior. Esto significa que los teclados acoplados que aparecen en esos bordes no activan un cambio de tamaño visual del viewport.

Anteriormente, el viewport visual permanecía fijo, lo que a menudo ocultaba los campos de entrada detrás del teclado. Cuando se cambia el tamaño del viewport, la parte visible de la página se puede desplazar de forma predeterminada, lo que garantiza que los usuarios puedan acceder al contenido oculto.

Límites y lógica de superposición

WebView solo debe recibir valores de inserción distintos de cero cuando los elementos de la IU del sistema (barras, cortes de pantalla o el teclado) se superpongan directamente con los límites de pantalla de WebView. Si una WebView no se superpone con estos elementos de la IU (por ejemplo, si una WebView está centrada en la pantalla y no toca las barras del sistema), debería recibir esas inserciones como cero.

Para anular esta lógica predeterminada y proporcionar al contenido web las dimensiones completas del sistema, independientemente de la superposición, usa el método setOnApplyWindowInsetsListener y devuelve el objeto windowInsets original sin modificar del objeto de escucha. Proporcionar dimensiones completas del sistema puede ayudar a garantizar la coherencia del diseño, ya que permite que el contenido web se alinee con el hardware del dispositivo, independientemente de la posición actual de WebView. Esto garantiza una transición fluida a medida que WebView se mueve o expande para tocar los bordes de la pantalla.

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

Administra eventos de cambio de tamaño

Dado que la visibilidad del teclado ahora activa un cambio de tamaño del viewport visual, es posible que el código web vea eventos de cambio de tamaño con más frecuencia. Los desarrolladores deben asegurarse de que su código no reaccione a estos eventos de cambio de tamaño borrando el enfoque del elemento. Esto crea un bucle de pérdida de enfoque y cierre del teclado que impide la entrada del usuario:

  1. El usuario se enfoca en un elemento de entrada.
  2. Aparece el teclado, lo que activa un evento de cambio de tamaño.
  3. El código del sitio web borra el enfoque en respuesta al cambio de tamaño.
  4. El teclado se oculta porque se perdió el enfoque.

Para mitigar este comportamiento, revisa los objetos de escucha del cliente web y asegúrate de que los cambios en el viewport no activen de forma involuntaria la función blur() de JavaScript ni los comportamientos de eliminación del enfoque.

Implementa el control de inserciones

La configuración predeterminada de WebView funciona automáticamente para la mayoría de las apps. Sin embargo, si tu app usa diseños personalizados (por ejemplo, si agregas tu propio padding para tener en cuenta la barra de estado o el teclado), puedes usar los siguientes enfoques para mejorar la forma en que el contenido web y la IU nativa funcionan en conjunto. Si tu IU nativa aplica relleno a un contenedor según WindowInsets, debes administrar estas inserciones correctamente antes de que lleguen a WebView para evitar el doble relleno.

El doble padding se produce cuando el diseño nativo y el contenido web aplican las mismas dimensiones de inserción, lo que genera un espaciado redundante. Por ejemplo, imagina un teléfono con una barra de estado de 40 px. Tanto la vista nativa como la WebView ven la inserción de 40 px. Ambos agregan 40 px de padding, lo que hace que el usuario vea un espacio de 80 px en la parte superior.

El enfoque de cero

Para evitar el doble padding, debes asegurarte de que, después de que una vista nativa use una dimensión de inserción para el padding, restablezcas esa dimensión a cero con Insets.NONE en un objeto WindowInsets nuevo antes de pasar el objeto modificado a la jerarquía de vistas de WebView.

Cuando apliques padding a una vista principal, por lo general, debes usar el enfoque de anulación estableciendo Insets.NONE en lugar de WindowInsetsCompat.CONSUMED. Devolver WindowInsetsCompat.CONSUMED podría funcionar en ciertas situaciones. Sin embargo, puede tener problemas si el controlador de tu app cambia las inserciones o agrega su propio padding. El enfoque de anulación no tiene estas limitaciones.

Evita el padding fantasma estableciendo en cero las inserciones

Si consumes las inserciones cuando la app ya pasó inserciones no consumidas o si las inserciones cambian (como cuando se oculta el teclado), consumirlas impide que el WebView reciba la notificación de actualización necesaria. Esto puede hacer que WebView conserve el padding fantasma de un estado anterior (por ejemplo, mantener el padding del teclado después de que se oculta).

En el siguiente ejemplo, se muestra una interacción interrumpida entre la app y WebView:

  1. Estado inicial: Inicialmente, la app pasa inserciones no consumidas (por ejemplo, displayCutout() o systemBars()) a WebView, que aplica internamente relleno al contenido web.
  2. Cambio de estado y error: Si la app cambia de estado (por ejemplo, se oculta el teclado) y decide controlar las inserciones resultantes devolviendo WindowInsetsCompat.CONSUMED.
  3. Notificación bloqueada: El consumo de las inserciones impide que el sistema Android envíe la notificación de actualización necesaria a través de la jerarquía de vistas a WebView.
  4. Padding fantasma: Debido a que WebView no recibe la actualización, conserva el padding del estado anterior, lo que provoca un padding fantasma (por ejemplo, mantener el padding del teclado después de que se oculta).

En su lugar, usa WindowInsetsCompat.Builder para establecer los tipos controlados en cero antes de pasar el objeto a las vistas secundarias. Esto le informa a WebView que ya se tuvieron en cuenta esas inserciones específicas mientras se habilitaba la notificación para que continuara por la jerarquía de vistas.

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();
});

Cómo dejar de participar

Para inhabilitar estos comportamientos modernos y volver al control de viewport heredado, haz lo siguiente:

  1. Inserciones de intercepción: Usa setOnApplyWindowInsetsListener o anula onApplyWindowInsets en una subclase WebView.

  2. Clear insets: Devuelve un conjunto consumido de insets (por ejemplo, WindowInsetsCompat.CONSUMED) desde el principio. Esta acción evita que la notificación de inserción se propague a WebView, lo que inhabilita el cambio de tamaño moderno del viewport y obliga a WebView a conservar su tamaño inicial del viewport visual.