Comprende las inserciones de ventana en WebView

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

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 para Android nativas:

Logro Se agregó la función Alcance
M136 Compatibilidad con displayCutout() y systemBars() a través de las inserciones de área segura de CSS. Solo WebViews de pantalla completa
M139 Compatibilidad con ime() (editor de método de entrada, que es un teclado) a través del cambio de tamaño del viewport visual Todos los WebViews
M144 Compatibilidad con displayCutout() y systemBars() Todas las WebViews (independientemente del estado de pantalla completa)

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

Mecánica principal

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 safe-area-inset-* de CSS. Esto permite que los desarrolladores eviten que sus propios elementos interactivos (como las barras de navegación) queden ocultos por los cortes o las barras de estado.

  • Cambio de tamaño del viewport visual con el editor de método de entrada (IME): A partir de M139, el editor de método de entrada (IME) cambia el tamaño del viewport visual directamente. Este mecanismo de cambio de tamaño también se basa en la intersección de 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, el viewport visual es 200 dp más pequeño que el tamaño del WebView. Este cambio de tamaño del viewport visual (tanto para el IME como para la intersección de WebView-Window) 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 fijos que aparecen en esos bordes no activan un cambio de tamaño del viewport visual.

Anteriormente, el viewport visual permanecía fijo, lo que solía ocultar 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ógica de límites y superposición

WebView debería recibir valores de inserción distintos de cero solo cuando los elementos de la IU del sistema (barras, cortes de pantalla o el teclado) se superpongan directamente con los límites de la 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 esos recuadros 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 las 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 la WebView. Esto garantiza una transición fluida a medida que WebView se mueve o se 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

Debido a 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 más frecuentes. Los desarrolladores deben asegurarse de que su código no reaccione a estos eventos de cambio de tamaño borrando el enfoque del elemento. Si lo haces, se creará un bucle de pérdida de enfoque y cierre del teclado que impedirá 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 lado de la Web para asegurarte de que los cambios en el viewport no activen de forma involuntaria la función de JavaScript blur() ni los comportamientos de eliminación de 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 juntos. 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 relleno doble.

El padding doble es una situación en la que 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 el recuadro de 40 px. Ambos agregan 40 dp de padding, lo que hace que el usuario vea un espacio de 80 dp en la parte superior.

El enfoque de reducción a cero

Para evitar el padding doble, 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 por la jerarquía de vistas al WebView.

Cuando apliques padding a una vista parental, por lo general, debes usar el enfoque de puesta a cero configurando 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 puesta a cero no tiene estas limitaciones.

Evita el padding fantasma con inserciones de cero

Si consumes los elementos de inserción cuando la app ya pasó elementos de inserción no consumidos o si estos cambian (como cuando se oculta el teclado), consumirlos evita que WebView reciba la notificación de actualización necesaria. Esto puede hacer que WebView retenga 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 rota entre la app y WebView:

  1. Estado inicial: Inicialmente, la app pasa inserciones no consumidas (por ejemplo, displayCutout() o systemBars()) al WebView, que aplica de forma interna padding al contenido web.
  2. Cambio de estado y error: Si la app cambia de estado (por ejemplo, se oculta el teclado) y elige 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. Relleno fantasma: Como WebView no recibe la actualización, conserva el relleno del estado anterior, lo que provoca un relleno fantasma (por ejemplo, mantener el relleno 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 informa a WebView que esos elementos de inserción específicos ya se tuvieron en cuenta, a la vez que permite que la notificación continúe en 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 inhabilitar la opción

Para inhabilitar estos comportamientos modernos y volver al manejo heredado del viewport, haz lo siguiente:

  1. Intercepta inserciones: Usa setOnApplyWindowInsetsListener o anula onApplyWindowInsets en una subclase WebView.

  2. Borra las inserciones: Devuelve un conjunto de inserciones consumidas (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 de forma eficaz el cambio de tamaño moderno del viewport y obliga a WebView a conservar su tamaño de viewport visual inicial.