Entender encartes de janela no WebView

O WebView gerencia o alinhamento do conteúdo usando duas janelas de visualização: a janela de visualização de layout (o tamanho da página) e a janela de visualização visual (a parte da página que o usuário realmente vê). Enquanto a janela de visualização de layout geralmente é estática, a janela de visualização visual muda dinamicamente quando os usuários dão zoom, rolam ou quando elementos da interface do sistema (como o teclado de software) aparecem.

Compatibilidade de recursos

O suporte do WebView para encartes de janela evoluiu ao longo do tempo para alinhar o comportamento do conteúdo da Web com as expectativas dos apps Android nativos:

Marco Recurso adicionado Escopo
M136 Suporte a displayCutout() e systemBars() com safe-area-insets do CSS. Somente WebViews em tela cheia.
M139 Suporte ao ime() (editor de método de entrada, que é um teclado) pelo redimensionamento da janela de visualização. Todas as WebViews.
M144 Compatibilidade de displayCutout() e systemBars(). Todas as WebViews (independentemente do estado de tela cheia).

Para ver mais informações, consulte WindowInsetsCompat.

Mecânica principal

A WebView processa as inserções por dois mecanismos principais:

  • Áreas seguras (displayCutout, systemBars): o WebView encaminha essas dimensões para o conteúdo da Web usando as variáveis CSS safe-area-inset-*. Isso permite que os desenvolvedores evitem que os próprios elementos interativos (como barras de navegação) sejam ocultados por cortes ou barras de status.

  • Redimensionamento da janela de visualização usando o editor de método de entrada (IME): a partir do M139, o IME redimensiona diretamente a janela de visualização. Esse mecanismo de redimensionamento também se baseia na interseção WebView-Window. Por exemplo, no modo multitarefa do Android, se a parte de baixo de uma WebView se estender 200 dp abaixo da parte de baixo da janela, a janela de visualização visual será 200 dp menor que o tamanho da WebView. Esse redimensionamento da janela de visualização (para a interseção do IME e da janela da WebView) é aplicado apenas à parte de baixo da WebView. Esse mecanismo não é compatível com o redimensionamento para sobreposição à esquerda, à direita ou na parte de cima. Isso significa que os teclados fixos que aparecem nessas bordas não acionam um redimensionamento visual da janela de visualização.

Antes, a janela de visualização visual permanecia fixa, muitas vezes ocultando campos de entrada atrás do teclado. Ao redimensionar a janela de visualização, a parte visível da página se torna rolável por padrão, garantindo que os usuários possam acessar o conteúdo oculto.

Lógica de limites e sobreposição

O WebView só vai receber valores de inserção diferentes de zero quando os elementos da interface do sistema (barras, cortes da tela ou o teclado) se sobrepuserem diretamente aos limites da tela do WebView. Se um WebView não se sobrepõe a esses elementos da interface (por exemplo, se um WebView está centralizado na tela e não toca nas barras de sistema), ele precisa receber essas inserções como zero.

Para modificar essa lógica padrão e fornecer ao conteúdo da Web as dimensões completas do sistema, independentemente da sobreposição, use o método setOnApplyWindowInsetsListener e retorne o objeto windowInsets original e não modificado do listener. Fornecer as dimensões completas do sistema pode ajudar a garantir a consistência do design, permitindo que o conteúdo da Web se alinhe ao hardware do dispositivo, independente da posição atual da WebView. Isso garante uma transição suave à medida que a WebView se move ou se expande para tocar nas bordas da tela.

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

Gerenciar eventos de redimensionamento

Como a visibilidade do teclado agora aciona um redimensionamento da janela de visualização, o código da Web pode ver eventos de redimensionamento mais frequentes. Os desenvolvedores precisam garantir que o código não reaja a esses eventos de redimensionamento limpando o foco do elemento. Isso cria um loop de perda de foco e fechamento do teclado que impede a entrada do usuário:

  1. O usuário foca em um elemento de entrada.
  2. O teclado aparece, acionando um evento de redimensionamento.
  3. O código do site limpa o foco em resposta ao redimensionamento.
  4. O teclado é ocultado porque o foco foi perdido.

Para mitigar esse comportamento, revise os listeners do lado da Web para garantir que as mudanças na janela de visualização não acionem acidentalmente a função JavaScript blur() ou comportamentos de limpeza de foco.

Implementar o processamento de inserções

As configurações padrão do WebView funcionam automaticamente para a maioria dos apps. No entanto, se o app usar layouts personalizados (por exemplo, se você adicionar seu próprio padding para considerar a barra de status ou o teclado), use as abordagens a seguir para melhorar o funcionamento do conteúdo da Web e da interface nativa. Se a interface nativa aplicar padding a um contêiner com base em WindowInsets, gerencie essas inserções corretamente antes que elas cheguem ao WebView para evitar o padding duplo.

O padding duplo é uma situação em que o layout nativo e o conteúdo da Web aplicam as mesmas dimensões de inserção, resultando em espaçamento redundante. Por exemplo, imagine um smartphone com uma barra de status de 40 px. Tanto a visualização nativa quanto a WebView veem o inset de 40 px. Ambos adicionam 40 px de padding, resultando em um espaço de 80 px na parte de cima.

A abordagem de zeramento

Para evitar o preenchimento duplo, é preciso garantir que, depois que uma visualização nativa usar uma dimensão de inserção para preenchimento, você redefina essa dimensão como zero usando Insets.NONE em um novo objeto WindowInsets antes de transmitir o objeto modificado pela hierarquia de visualização para a WebView.

Ao aplicar padding a uma visualização mãe, geralmente é recomendável usar a abordagem de zerar, definindo Insets.NONE em vez de WindowInsetsCompat.CONSUMED. Retornar WindowInsetsCompat.CONSUMED pode funcionar em determinadas situações. No entanto, podem ocorrer problemas se o gerenciador do app mudar os encartes ou adicionar o próprio preenchimento. A abordagem de zeragem não tem essas limitações.

Evitar o preenchimento fantasma zerando as inserções

Se você consumir as inserções quando o app tiver transmitido inserções não consumidas anteriormente ou se as inserções mudarem (como o teclado oculto), o consumo delas vai impedir que o WebView receba a notificação de atualização necessária. Isso pode fazer com que a WebView retenha o padding fantasma de um estado anterior (por exemplo, mantendo o padding do teclado depois que ele é ocultado).

O exemplo a seguir mostra uma interação corrompida entre o app e o WebView:

  1. Estado inicial:o app inicialmente transmite inserções não consumidas (por exemplo, displayCutout() ou systemBars()) para a WebView, que aplica internamente o padding ao conteúdo da Web.
  2. Mudança de estado e erro:se o app mudar de estado (por exemplo, o teclado é ocultado) e optar por processar as inserções resultantes retornando WindowInsetsCompat.CONSUMED.
  3. Notificação bloqueada:o consumo dos encartes impede que o sistema Android envie a notificação de atualização necessária pela hierarquia de visualização para a WebView.
  4. Padding fantasma:como a WebView não recebe a atualização, ela mantém o padding do estado anterior, causando um padding fantasma (por exemplo, mantendo o padding do teclado depois que ele é ocultado).

Em vez disso, use WindowInsetsCompat.Builder para definir os tipos processados como zero antes de transmitir o objeto para as visualizações filhas. Isso informa à WebView que essas inserções específicas já foram consideradas ao permitir que a notificação continue na hierarquia de visualização.

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

Como fazer a desativação

Para desativar esses comportamentos modernos e retornar ao processamento de janela de visualização legado, faça o seguinte:

  1. Interceptar inserções:use setOnApplyWindowInsetsListener ou substitua onApplyWindowInsets em uma subclasse WebView.

  2. Limpar encartes:retorna um conjunto consumido de encartes (por exemplo, WindowInsetsCompat.CONSUMED) desde o início. Essa ação impede que a notificação de inserção seja propagada para a WebView, desativando o redimensionamento moderno da janela de visualização e forçando a WebView a manter o tamanho inicial da janela de visualização.