Search code examples
javajavafxgridviewcontrolsfx

GridView only create Images when they would be visible


Hei there

So I have the following problem. I have around 1500 images of playing cards. I want to display them in a "Gallery" where you could scroll through them. I was able to create a GridView with the ImageCell object and I was also able to add images to it. Now my problem is that if I add all Image's at once logically Java crashes because of a heap error. I have the image url's (local files) in a list. How could I implement that it only load lets say 15 images. If I then scroll it loads the next 15 and unloads the old ones. So it would only load the images of the actual visible images and not all 1500. How would I do this? I am completely out of ideas. The Platform.runLater() is needed because some sort of issue with ControlsFX

Here my code:

    public void initialize() {

    GridView<Image> gridView = new GridView<>();
    gridView.setCellFactory(gridView1 -> new ImageGridCell(true));
    Image image = new Image("C:\\Users\\nijog\\Downloads\\cardpictures\\01DE001.png");

    gridView.setCellWidth(340);
    gridView.setCellHeight(512);

    //Platform.runLater(()-> {
    //    for (int i = 0; i < 5000; i++){
    //        gridView.getItems().add(image);
    //    }
    //});

    Platform.runLater(() -> gridView.getItems().addAll(createImageListFromCardFiles()));

    borderPane.setCenter(gridView);

}

protected List<Image> createImageListFromCardFiles(){

    List<Image> imageViewList = new ArrayList<>();
    App.getCardService().getCardArray().stream()
            //.filter(Card::isCollectible)
            .sorted(Comparator.comparingInt(Card::getCost))
            .sorted(Comparator.comparing(Card::isChampion).reversed())
            .skip(0)
            //.limit(100)
            .forEach(card -> {
                try {
                    String url = String.format(App.pictureFolderPath +"%s.png", card.getCardCode());
                    imageViewList.add(new Image(url));
                } catch (Exception e) {
                    System.out.println("Picture file not found [CardCode = " + card.getCardCode() + "]");
                    App.logger.writeLog(Logger.Operation.EXCEPTION, "Picture file not found [CardCode = " + card.getCardCode() + "]");
                }
            });
    return imageViewList;
}

Solution

  • You might not need to use the strategy you describe. You're displaying the images in cells of size 340x512, which is 174,080 pixels. Image storage is 4 bytes per pixel, so this is 696,320 bytes per image; 1500 of them will consume about 1GB. You just need to make sure you load the image at the size you are displaying it (instead of its native size):

    // imageViewList.add(new Image(url));
    imageViewList.add(new Image(url, 340, 512, true, true, true));
    

    If you need an image at full size later (e.g. if you want the user to select an image from your grid view and display it in a bigger pane), you'd just need to reload it from the url.

    If you do need to implement the strategy you describe, GridView supports that out of the box. Just keep a list of the URLs, instead of the Images, and use a custom GridCell to load the image as needed. This will consume significantly less memory, at the cost of a lot more I/O (loading the images) and CPU (parsing the image format).

    Make the items for the GridView the image urls, stored as Strings.

    Then you can do something like:

    GridView<String> gridView = new GridView<>();
    gridView.getItems().addAll(getAllImageURLs());
    
    gridView.setCellFactory(gv -> new GridCell<>() {
        private final ImageView imageView = new ImageView();
    
        {
            imageView.fitWidthProperty().bind(widthProperty());
            imageView.fitHeightProperty().bind(heightProperty());
            imageView.setPreserveRatio(true);
        }
    
        @Override
        protected void updateItem(String url, boolean empty) {
            super.updateItem(url, empty);
            if (empty || url == null) {
                setGraphic(null);
            } else {
                double w = getGridView().getCellWidth();
                double h = getGridView().getCellHeight();
                imageView.setImage(new Image(url, w, h, true, true, true));
                setGraphic(imageView);
            }
        }
    });
    
    
    protected List<String> getAllImageURLs(){
    
        return App.getCardService().getCardArray().stream()
                // isn't the first sort redundant here?
                .sorted(Comparator.comparingInt(Card::getCost))
                .sorted(Comparator.comparing(Card::isChampion).reversed())
                .map(card -> String.format(App.pictureFolderPath +"%s.png", card.getCardCode()))
                .collect(Collectors.toList());
    }