3

While looking at Pagination, the question of rendering complex pages arose. The API example, et al., typically specify a pageFactory that simply constructs a new control each time it is called. Indeed, profiling the example below while paging showed minimal memory pressure, with a flurry of new instances that were promptly collected. What can I do if growing complexity changes the picture?

simple page

import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Pagination;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

/** @see https://stackoverflow.com/q/76359690/230513 */

public class PaginationSample extends Application {

    private static final int N = 100;

    private record Item(String name, Color color) {

        private static final Random r = new Random();

        public static Item ofRandom() {
            var s = (char) ('A' + r.nextInt(26))
                + String.valueOf(r.nextInt(900_000) + 100_000);
            var c = Color.rgb(r.nextInt(255), r.nextInt(255), r.nextInt(255));
            return new Item(s, c);
        }
    }

    @Override
    public void start(Stage stage) {
        stage.setTitle("PaginationTStreamest");
        List<Item> items = Stream.generate(Item::ofRandom)
            .limit(N).collect(Collectors.toList());
        var pagination = new Pagination(items.size(), 0);
        pagination.setPageFactory((i) -> createItemPane(items.get(i)));
        stage.setScene(new Scene(pagination));
        stage.show();
        pagination.requestFocus();
    }

    private StackPane createItemPane(Item item) {
        var pane = new StackPane();
        pane.setPadding(new Insets(16));
        var label = new Label(item.name, new Rectangle(320, 240, item.color));
        label.setTextFill(item.color.invert());
        label.setStyle("-fx-font-family: serif; -fx-font-size: 36;");
        label.setContentDisplay(ContentDisplay.CENTER);
        var button = new Button("Button");
        StackPane.setAlignment(button, Pos.BOTTOM_RIGHT);
        button.setOnAction((e) -> {
            System.out.println(item);
        });
        pane.getChildren().addAll(label, button);
        return pane;
    }

    public static void main(String[] args) {
        launch(args);
    }
}
trashgod
  • 203,806
  • 29
  • 246
  • 1,045

1 Answers1

5

Profiling is the correct thing to do in this case, but optimizing the renderer's preparation is probably warranted. Like ListView and TableView, Pagination makes flyweight rendering easy:

  • Construct the minimal number of reusable display objects.

  • When called to render, update existing objects to effect a change.

In the RenderPane below, init() establishes a single pane, chart and button, while the fill() method invoked by the page factory's callback prepares the renderer with details of a specific Item for each page. Note that a single chart and data collection suffices; the listening chart will update itself in response to the change.

An ad hoc constructor and factory invocation in comments may be used for comparison. The benefit is measurable, suggesting it should scale well.

A related chart example is seen here, and additional Pagination topics are addressed here.

Caveats: As noted in comments by @Slaw below, the PaginationSkin animates the transition from one page to the next. With just a single view component, the animation is serviceable but less appealing. Moreover, the animation may interfere with subsequent updates. Among possible accommodations that may be required:

  • Disable the component's animation, shown in the example below:

      chart.setAnimated(false);
    
  • Schedule the the component's update to follow the animation.

      public Pane fill(int i, Item item) {
          Platform.runLater(() -> {
              …
          });
          return pane;
    

Complex page

PaginationTest.java

import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.chart.PieChart;
import javafx.scene.control.Button;
import javafx.scene.control.Pagination;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;

/** @see https://stackoverflow.com/a/76359691/230513 */
public class PaginationTest extends Application {

    private static final int N = 100;

    private record Item(String name, Color color) {

        private static final Random r = new Random();

        public static Item ofRandom() {
            var s = (char) ('A' + r.nextInt(26))
                + String.valueOf(r.nextInt(900_000) + 100_000);
            var c = Color.rgb(r.nextInt(255), r.nextInt(255), r.nextInt(255));
            return new Item(s, c);
        }
    }

    @Override
    public void start(Stage stage) {
        stage.setTitle("PaginationTest");
        List<Item> items = Stream.generate(Item::ofRandom)
            .limit(N).collect(Collectors.toList());
        var pagination = new Pagination(items.size(), 0);
        var renderer = new RenderPane();
        pagination.setPageFactory((i) -> renderer.fill(items.get(i)));
//        pagination.setPageFactory((i) -> {
//            var renderer = new RenderPane(items.get(i));
//            return renderer.pane;
//        });
        stage.setScene(new Scene(pagination));
        stage.show();
        pagination.requestFocus();
    }

    private final static class RenderPane {

        private final StackPane pane = new StackPane();
        private final PieChart chart = new PieChart();
        private final Circle circle = new Circle(10);
        private final Button button = new Button("Details", circle);

        public RenderPane() {
            init();
        }

//        public RenderPane(Item item) {
//            init();
//            fill(item);
//        }

        private void init() {
            pane.setPadding(new Insets(16));
            StackPane.setAlignment(button, Pos.BOTTOM_RIGHT);
            chart.setAnimated(false);
            chart.getData().add(0, new PieChart.Data("Red", 0));
            chart.getData().add(1, new PieChart.Data("Green", 0));
            chart.getData().add(2, new PieChart.Data("Blue", 0));
            chart.getStylesheets().add("PaginationTest.css");
            button.setGraphic(circle);
            pane.getChildren().addAll(chart, button);
        }

        public Pane fill(Item item) {
            chart.setTitle(item.name);
            chart.getData().get(0).setPieValue(item.color.getRed());
            chart.getData().get(1).setPieValue(item.color.getGreen());
            chart.getData().get(2).setPieValue(item.color.getBlue());
            circle.setFill(item.color);
            button.setOnAction((e) -> {
                System.out.println(item);
            });
            return pane;
        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}

PaginationTest.css

PaginationTest.css 
.default-color0.chart-pie { -fx-pie-color: #FF0000; }
.default-color1.chart-pie { -fx-pie-color: #00FF00; }
.default-color2.chart-pie { -fx-pie-color: #0000FF; }
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
  • 1
    Does returning the _same_ node each time not mess with pagination's animation? – Slaw May 29 '23 at 20:49
  • @Slaw: Yes, thank you for the reminder; disabling the chart animation resolved the problem, but I am still trying to understand why. I'd welcome your insight or answer. – trashgod May 29 '23 at 22:30
  • 2
    Internally, `PaginationSkin` keeps a "current pane" and a "next pane". These are stack panes and contain the nodes returned by the page factory. And it's these panes that are animated. When you navigate to another page, the page factory is called, and the returned node is put in "next pane". When the animation is finished, the "next pane" becomes the "current pane" and vice versa. By returning the _same_ node from the page factory every time, the node is implicitly removed from the "current pane" because a node cannot belong to two parents. I would guess this messes with the chart's animation. – Slaw May 29 '23 at 23:49
  • 1
    I expected this to actually mess with the _pagination's_ animation (the left-to-right/right-to-left transitions). But trying your code, that actually continues to work well (you just don't see the page _exiting_ because that page's node was moved to the _entering_ page). But the _chart's_ animation, when enabled, doesn't seem to run until a UI refresh is forced. Disabling the chart animation appears to "fix" this well enough. Perhaps another (better?) solution would be to have two charts that are alternated by the page factory. Not sure if that won't run into the same problem, however. – Slaw May 29 '23 at 23:54
  • 1
    Though maybe a third solution is to just put the "change the chart data" code inside a `Platform.runLater` call, so that it runs after the chart has been added to the new parent? – Slaw May 29 '23 at 23:58