Comprendre les encarts de fenêtre dans WebView

WebView gère l'alignement du contenu à l'aide de deux fenêtres d'affichage : la fenêtre d'affichage de mise en page (la taille de la page) et la fenêtre d'affichage visuelle (la partie de la page que l'utilisateur voit réellement). Bien que la fenêtre d'affichage de la mise en page soit généralement statique, la fenêtre d'affichage visuelle change de manière dynamique lorsque les utilisateurs font un zoom avant ou arrière, font défiler l'écran ou lorsque des éléments de l'UI système (tels que le clavier logiciel) apparaissent.

Compatibilité des fonctionnalités

La compatibilité de WebView avec les encarts de fenêtre a évolué au fil du temps pour aligner le comportement du contenu Web sur les attentes des applications Android natives :

Étape importante Fonctionnalité ajoutée Champ d'application
M136 displayCutout() et systemBars() sont compatibles avec les encarts safe-area-insets CSS. WebViews en plein écran uniquement.
M139 Prise en charge de ime() (éditeur de méthode de saisie, qui est un clavier) grâce au redimensionnement visuel de la fenêtre d'affichage. Tous les composants WebView.
M144 displayCutout() et systemBars() sont acceptés. Toutes les WebViews (quel que soit l'état plein écran).

Pour en savoir plus, consultez WindowInsetsCompat.

Mécanismes de base

WebView gère les encarts grâce à deux mécanismes principaux :

  • Zones de sécurité (displayCutout, systemBars) : WebView transmet ces dimensions au contenu Web via les variables CSS safe-area-inset-*. Cela permet aux développeurs d'empêcher que leurs propres éléments interactifs (comme les barres de navigation) ne soient masqués par des encoches ou des barres d'état.

  • Redimensionnement visuel de la fenêtre d'affichage à l'aide de l'éditeur de méthode de saisie (IME) : à partir de M139, l'éditeur de méthode de saisie (IME) redimensionne directement la fenêtre d'affichage visuelle. Ce mécanisme de redimensionnement est également basé sur l'intersection WebView-Window. Par exemple, en mode multitâche Android, si le bas d'une WebView s'étend de 200 dp sous le bas de la fenêtre, la fenêtre d'affichage visuelle est 200 dp plus petite que la taille de la WebView. Ce redimensionnement de la fenêtre d'affichage visuelle (pour l'intersection IME et WebView-Window) n'est appliqué qu'au bas de la WebView. Ce mécanisme n'est pas compatible avec le redimensionnement pour le chevauchement à gauche, à droite ou en haut. Cela signifie que les claviers ancrés qui apparaissent sur ces bords ne déclenchent pas de redimensionnement visuel de la fenêtre d'affichage.

Auparavant, la fenêtre d'affichage visuelle restait fixe, ce qui masquait souvent les champs de saisie derrière le clavier. En redimensionnant la fenêtre d'affichage, la partie visible de la page devient défilable par défaut, ce qui permet aux utilisateurs d'accéder au contenu masqué.

Limites et logique de chevauchement

WebView ne doit recevoir des valeurs d'encart non nulles que lorsque des éléments de l'UI système (barres, encoches ou clavier) se chevauchent directement avec les limites de l'écran WebView. Si un WebView ne chevauche pas ces éléments d'UI (par exemple, si un WebView est centré sur l'écran et ne touche pas les barres système), il doit recevoir ces encarts comme étant nuls.

Pour remplacer cette logique par défaut et fournir au contenu Web les dimensions complètes du système, quel que soit le chevauchement, utilisez la méthode setOnApplyWindowInsetsListener et renvoyez l'objet windowInsets d'origine et non modifié à partir de l'écouteur. Fournir les dimensions complètes du système peut aider à assurer la cohérence de la conception en permettant au contenu Web de s'aligner sur le matériel de l'appareil, quelle que soit la position actuelle de WebView. Cela garantit une transition fluide lorsque la WebView se déplace ou s'étend pour toucher les bords de l'écran.

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

Gérer les événements de redimensionnement

Étant donné que la visibilité du clavier déclenche désormais un redimensionnement de la fenêtre d'affichage visuelle, le code Web peut voir des événements de redimensionnement plus fréquents. Les développeurs doivent s'assurer que leur code ne réagit pas à ces événements de redimensionnement en effaçant la sélection de l'élément. Cela crée une boucle de perte de sélection et de fermeture du clavier qui empêche l'utilisateur de saisir du texte :

  1. L'utilisateur se concentre sur un élément d'entrée.
  2. Le clavier s'affiche, ce qui déclenche un événement de redimensionnement.
  3. Le code du site Web efface la sélection en réponse au redimensionnement.
  4. Le clavier se masque, car la sélection a été perdue.

Pour atténuer ce comportement, examinez les écouteurs côté Web afin de vous assurer que les modifications de la fenêtre d'affichage ne déclenchent pas involontairement la fonction JavaScript blur() ni les comportements de suppression de la mise au point.

Implémenter la gestion des encarts

Les paramètres par défaut de WebView fonctionnent automatiquement pour la plupart des applications. Toutefois, si votre application utilise des mises en page personnalisées (par exemple, si vous ajoutez votre propre marge intérieure pour tenir compte de la barre d'état ou du clavier), vous pouvez utiliser les approches suivantes pour améliorer la façon dont le contenu Web et l'UI native fonctionnent ensemble. Si votre UI native applique une marge intérieure à un conteneur en fonction de WindowInsets, vous devez gérer correctement ces encarts avant qu'ils n'atteignent la WebView pour éviter une double marge intérieure.

Le double espacement se produit lorsque la mise en page native et le contenu Web appliquent les mêmes dimensions d'encart, ce qui entraîne un espacement redondant. Par exemple, imaginez un téléphone avec une barre d'état de 40 px. La vue native et la WebView voient l'encart de 40 px. Les deux ajoutent 40 px de marge intérieure, ce qui fait que l'utilisateur voit un espace de 80 px en haut.

L'approche Zéro

Pour éviter le double espacement, vous devez vous assurer qu'après qu'une vue native a utilisé une dimension d'encart pour l'espacement, vous réinitialisez cette dimension à zéro à l'aide de Insets.NONE sur un nouvel objet WindowInsets avant de transmettre l'objet modifié dans la hiérarchie des vues à la WebView.

Lorsque vous appliquez une marge intérieure à une vue parente, vous devez généralement utiliser l'approche de mise à zéro en définissant Insets.NONE au lieu de WindowInsetsCompat.CONSUMED. Le retour de WindowInsetsCompat.CONSUMED peut fonctionner dans certaines situations. Toutefois, il peut rencontrer des problèmes si le gestionnaire de votre application modifie les encarts ou ajoute sa propre marge intérieure. L'approche de mise à zéro ne présente pas ces limites.

Éviter les marges intérieures fantômes en définissant les encarts sur zéro

Si vous consommez les encarts lorsque l'application a précédemment transmis des encarts non consommés, ou si les encarts changent (comme le masquage du clavier), leur consommation empêche la WebView de recevoir la notification de mise à jour nécessaire. Cela peut entraîner la conservation d'une marge intérieure fantôme par WebView à partir d'un état précédent (par exemple, la conservation de la marge intérieure du clavier après sa masquage).

L'exemple suivant montre une interaction interrompue entre l'application et WebView :

  1. État initial : l'application transmet initialement les encarts non consommés (par exemple, displayCutout() ou systemBars()) à la WebView, qui applique en interne une marge intérieure au contenu Web.
  2. Changement d'état et erreur : si l'application change d'état (par exemple, le clavier se masque) et choisit de gérer les encarts résultants en renvoyant WindowInsetsCompat.CONSUMED.
  3. Notification bloquée : la consommation des encarts empêche le système Android d'envoyer la notification de mise à jour nécessaire dans la hiérarchie des vues vers WebView.
  4. Marge fantôme : comme la WebView ne reçoit pas la mise à jour, elle conserve la marge intérieure de l'état précédent, ce qui entraîne une marge fantôme (par exemple, la marge du clavier est conservée après que le clavier est masqué).

Utilisez plutôt WindowInsetsCompat.Builder pour définir les types traités sur zéro avant de transmettre l'objet aux vues enfants. Cela indique à la WebView que ces encarts spécifiques ont déjà été pris en compte tout en permettant à la notification de continuer à descendre dans la hiérarchie des vues.

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

Comment se désinscrire ?

Pour désactiver ces comportements modernes et revenir à l'ancienne gestion du viewport, procédez comme suit :

  1. Inserts d'interception : utilisez setOnApplyWindowInsetsListener ou remplacez onApplyWindowInsets dans une sous-classe WebView.

  2. Effacer les encarts : renvoie un ensemble d'encarts consommés (par exemple, WindowInsetsCompat.CONSUMED) depuis le début. Cette action empêche la propagation de la notification d'encart dans la WebView, ce qui désactive le redimensionnement moderne de la fenêtre d'affichage et force la WebView à conserver sa taille de fenêtre d'affichage visuelle initiale.