4

I have a messaging application running in Android which has the setup like in setup of the screen the order is as below

<View>
<BorderPane>
  <center>
    <ScrollPane>
       <content>
         <VBox> //issue is here
       </content>
    <ScrollPane>
  <center>
  <bottom>
    <TextField>
  <bottom>
</BorderPane>
</View>

When I add children to VBox with

VBox.getChildren().add(TextLabel);

The ScrollPane gets new VBox and shows that on the screen. However when i add more children that what current screen can fit i scroll to end of the ScrollPane by setting vvalueProperty();

ScrollPane.vvalueProperty().bind(VBox.heightProperty());

(Above code is essential to recreate the issue)

This works perfectly fine when running it on computer but on mobile i have this weird issue where scrollPane drops VBox when i add more children than what can be fit on the screen. And when i click on the VBox area the screen refreshes and i get the desired content on the screen Video demonstrating ScrollBar issue in gluon

For convenience i have set following color code

ScrollBar - Red

VBox - Blue

As an alternative to binding I also tried

 ScrollBar.setVvalue(1.0);

setVvalue() did not have same issue but this on the other hand was not showing the last message in the view. Right now i have tried all possible combinations including replacing VBox with FlowPane and observed same behavior.

Sachin Hegde
  • 151
  • 10
  • Is all of this run on the JavaFx thread? Also, have you tried it on a different mobile device? – Hypnic Jerk Jun 19 '18 at 13:40
  • Yes, it runs on the JavaFX thread, i have also tried it on different android devices with same issue. – Sachin Hegde Jun 19 '18 at 13:45
  • Is there a reason that some of the text you send in your video does not appear in the vbox? That may be an underlying problem that causes this. – Hypnic Jerk Jun 19 '18 at 13:49
  • The initial few texts are on the top of the scrollPane which are out of view when i open keyboard, they can be seen if i close the keyboard view. Keeping those texts in view is something i need to figure out as well. @Hypnic Jerk – Sachin Hegde Jun 19 '18 at 14:03
  • I created a small app that did something similar, put a Label in a VBox that is in a ScrollPane, and I had no droppage. I did not try setting any vValues or binding that property. I also used the Gluon built in View instead of BorderPane. Without seeing more of your code I am out of ideas. – Hypnic Jerk Jun 19 '18 at 14:13
  • @Hypnic Jerk. The Issue cannot be recreated without binding as that ensures last message in the chat is shown(like any messaging application shows last part of the conversation in view). I can understand why you were not able to recreate without dynamic binding. Just to reiterate, the issue occurs when i have dynamic binding. hence `ScrollPane.vvalueProperty().bind(VBox.heightProperty());` is a essential property. As an alternative to binding, I tried vValue. However this does not have same issue but that fails to show last message on the screen(it hides it below the textfield) – Sachin Hegde Jun 19 '18 at 14:55

1 Answers1

1

I can reproduce your issue on an Android device. Somehow, as discussed in the comments above, the binding of the vertical scroll position is causing some race condition.

Instead of trying to find out the cause of that issue, I'd rather remove the binding and propose a different approach to get the desired result: the binding is a very strong condition in this case.

When you try to do in the same pass this:

vBox.getChildren().add(label);
scrollPane.setVvalue(vBox.getHeight());

you already noticed and mentioned that the scroll position wasn't properly updated and you were missing the last item added to the vBox.

This can be explained easily: when you add a new item to the box, there is a call to layoutChildren() that will take some time to be performed. At least it will take another pulse to get the correct value.

But since you try to set immediately the vertical scroll position, the value vBox.getHeight() will still return the old value. In other words, you have to wait a little bit to get the new value.

There are several ways to do it. The most straightforward is with a listener to the box's height property:

vBox.heightProperty().addListener((obs, ov, nv) -> 
    scrollPane.setVvalue(nv.doubleValue()));

As an alternative, after adding the item to the box, you could use:

vBox.getChildren().add(label);
Platform.runLater(() -> scrollPane.setVvalue(vBox.getHeight()));

But this doesn't guarantee that the call won't be done immediately. So it is better to do a PauseTransition instead, where you can control the timing:

vBox.getChildren().add(label);

PauseTransition pause = new PauseTransition(Duration.millis(30));
pause.setOnFinished(f -> scrollPane.setVvalue(vBox.getHeight()));
pause.play();

As a suggestion, you could also do a nice transition to slide in the new item.

Alternative solution

So far, you are using an ScrollPane combined with a VBox to add a number of items, allowing scrolling to the first item on the list but keeping the scroll position always at the bottom so the last item added is fully visible. While this works fine (with my proposal above to avoid the binding), you are adding many nodes to a non virtualized container.

I think there is a better alternative, with a ListView (or better a CharmListView that will allow headers). With the proper CellFactory you can have exactly the same visuals, and you can directly scroll to the last item. But the main advantage of this control is its virtualFlow, that will manage for you a limited number of nodes while you have many more items added to a list.

This is just a short code snippet to use a ListView control for your chat application:

ListView<String> listView = new ListView<>();
listView.setCellFactory(p -> new ListCell<String>() {
    private final Label label;
    {
        label = new Label(null, MaterialDesignIcon.CHAT_BUBBLE.graphic());
        label.setMaxWidth(Double.MAX_VALUE);
        label.setPrefWidth(this.getWidth() - 60);
        label.setPrefHeight(30);
    }
    @Override
    protected void updateItem(String item, boolean empty) {
        super.updateItem(item, empty);
        if (item != null && ! empty) {
            label.setText(item);
            label.setAlignment(getIndex() % 2 == 0 ? Pos.CENTER_LEFT : Pos.CENTER_RIGHT);
            setGraphic(label);
        } else {
            setGraphic(null);
        }
    }

});
setCenter(listView);

and to add a new item and scroll to it, you just need:

listView.getItems().add("Text " + (listView.getItems().size() + 1));
listView.scrollTo(listView.getItems().size() - 1);

Of course, with the proper styling you can remove the lines between rows, and create the same visuals as with the scrollPane.

José Pereda
  • 44,311
  • 7
  • 104
  • 132