Developpez.com - Rubrique JavaScript

Le Club des Développeurs et IT Pro

Prendre le contrôle des barres de défilement de WebView via JavaScript

Un billet de Bouye

Le 09/09/2015, par bouye, Rédacteur/Modérateur
Avant de commencer, je tiens à préciser que depuis le JDK 1.8.0_60, le contrôle WebView souffre d'un très gros bogue qui fait que le WebEngine n'est plus capable de charger des ressources (fichiers image, CSS, JavaScript) incluses dans un JAR lorsqu’on charge un fichier HTML inclus dans cette même archive. Ceci casse pas mal de solutions avec des navigateurs embarqués qui permettaient de charger, par exemple, des pages GoogleMap, etc. Voir [JDK-8134922] - Resource files are not loaded by the WebEngine when bundled in a jar file sur le JDK Bug System. Ces applications devraient toujours pouvoir fonctionner correctement avec le JDK 1.8.0_51.

Ces derniers temps, pour vérifier les valeurs d'un objet paramètre dans une UI, je faisais une sortie texte en affichant un objet Text dans un TextFlow inclus dans un StackPane, lui-meme placé dans un ScrollPane. Comme l'objet paramètre n'est pas observable, j'ai mis en place un ScheduleService qui va régulièrement régénérer un rapport textuel et afficher le tout à l’écran :
Code Java :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private final TextFlow textFlow = new TextFlow(); 
private final StackPane textStack = new StackPane(); 
private final ScrollPane scrollPane = new ScrollPane(); 
  
[...] 
  
textFlow.setId("textFlow"); // NOI18N. 
// 
textStack.setId("textStack"); // NOI18N. 
textStack.getChildren().setAll(textFlow); 
// 
scrollPane.setId("scrollPane"); // NOI18N.; 
scrollPane.setFitToWidth(true); 
scrollPane.setContent(textStack); 
getChildren().add(scrollPane);

Et dans le service qui met à jour l'affichage :
Code Java :
1
2
3
4
5
6
7
8
9
10
11
service.setOnSucceeded(workerStateEvent -> { 
    final String result = (String) workerStateEvent.getSource().getValue();  // Ici le résultat est au format texte. 
    if (!Objects.equals(oldResult, result)) { 
        oldResult = result; 
        final Text text = (result == null) ? null : new Text(result); 
        textFlow.getChildren().clear(); 
        if (text != null) { 
            textFlow.getChildren().setAll(text); 
        } 
    } 
});

Jusque là, rien de bien complexe ; même si j'ai fait défilé le ScrollPane et que le Text se met à jour, le défilement ne se réinitialise pas et la vue conserve ses coordonnées d’elle-même pour ne pas gêner l'utilisateur.

Étant donné que les paramètres se multiplient, j'ai décidé d'opter pour une version HTML du rapport ; ce qui permet de mettre un peu mieux les paramètres importants en évidences et de condenser l'affichage en jouant sur la taille de la police ou avec des <div> placés sur un flow layout.
Code Java :
1
2
3
4
5
6
7
private final WebView webView = new WebView(); 
  
[...] 
  
webView.setId("webView"); // NOI18N. 
webView.setContextMenuEnabled(false); 
getChildren().add(webView);

Et dans le service qui met à jour l'affichage :
Code Java :
1
2
3
4
5
6
7
service.setOnSucceeded(workerStateEvent -> { 
    final String result = (String) workerStateEvent.getSource().getValue(); // Ici le résultat est au format HTML. 
    if (!Objects.equals(oldResult, result)) { 
        oldResult = result; 
        webView.getEngine().loadContent(result); 
    } 
});

Le principal soucis rencontré est que, lorsque le service génère un nouveau rapport, l'affichage se réinitialise : le défilement vertical de la page se remet à zéro et l'utilisateur perd sa position dans la vue.

Or WebView est un composant complexe "opaque" (dans le sens où on ignore comment interagir avec ses sous-parties) qui n'offre aucune API permettant de manipuler ses barres de défilement. J'ai essayé de bidouiller un peu via la méthode lookup() en cherchant l'id ou la classe CSS des barres de défilement mais cela n'a rien donné, au final, de concret ou du moins qui fonctionne correctement.

Il existe cependant une manière bien plus simple et rapide de régler ce problème : invoquer via le WebEngine des bouts de code JavaScript qui permettent de connaitre le défilement actuel de la page et, une fois la nouvelle page chargée, restaurer cette valeur. Nous allons commencer par ajouter un écouteur :
Code Java :
1
2
3
4
5
private final ChangeListener<Worker.State> engineLoadWorkerListener = (observable, oldValue, newValue) -> { 
    if (newValue == Worker.State.SUCCEEDED) { 
        restoreScrollAmount(); 
    } 
};

Cet écouteur sera invoqué lors des changements d’état du service utilisé par le WebEngine lorsque ce dernier charge des pages web. Il va nous permettre de restaurer le défilement dans la page web lorsque le chargement de la nouvelle page sera terminé.

Et ces deux méthodes qui permettent de sauvegarder et de restaurer les valeurs de défilement de la page :
Code Java :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private int xScroll = 0; 
private int yScroll = 0; 
  
// Sauvegarde le défilement de la page web. 
private void storeScrollAmount() { 
    xScroll = (Integer) webView.getEngine().executeScript("window.pageXOffset"); // NOI18N. 
    yScroll = (Integer) webView.getEngine().executeScript("window.pageYOffset"); // NOI18N. 
} 
  
// Restaure le défilement dans la page web. 
private void restoreScrollAmount() { 
    final String command = String.format("window.scrollTo(%d, %d)", xScroll, yScroll); // NOI18N. 
    webView.getEngine().executeScript(command); 
}

Et il nous suffit alors d’insérer la ligne suivante à la création du WebView :
Code Java :
webView.getEngine().getLoadWorker().stateProperty().addListener(engineLoadWorkerListener);

Et dans le service qui met à jour l'affichage :
Code Java :
1
2
3
4
5
6
7
8
service.setOnSucceeded(workerStateEvent -> { 
    final String result = (String) workerStateEvent.getSource().getValue(); // Ici le résultat est au format HTML. 
    if (!Objects.equals(oldResult, result)) { 
        oldResult = result; 
        storeScrollAmount(); // Sauvegarde des valeurs de défilement. 
        webView.getEngine().loadContent(result); 
    } 
});

Désormais, nous sauvegardons les valeurs de défilement avant de charger une nouvelle page dans le WebView. Et nous les restaurons lorsque ce dernier a fini de charger la nouvelle page.
  Billet blog