How load javafx.scene.image.Image by demand?

前端 未结 3 1227
予麋鹿
予麋鹿 2021-01-29 06:04

Is it possible to discard loaded content of Image and later load it again? Is it possible to load it on demand?

Can I have ImageView which loads it\'s image only on show

3条回答
  •  别那么骄傲
    2021-01-29 06:28

    The Image class is essentially immutable with respect to its image data, in the sense that you can specify a source for the image data at construction time, and then cannot modify it via the API subsequently.

    The ImageView class provides functionality for displaying an image in the UI. The ImageView class is mutable, in the sense that you can change the image it displays.

    The basic strategy you need to implement "tiled images" functionality is to create a virtualized container, which has a collection of "cells" or "tiles" which are reused to display different content. This is essentially how controls such as ListView, TableView, and TreeView are implemented in JavaFX. You may also be interested in Tomas Mikula's Flowless implementation of the same kind of idea.

    So to implement "tiled images" functionality, you could use an array of ImageViews as the "cells" or "tiles". You can place these in a pane and implement panning/scrolling in the pane, and when image views scroll out of view, reuse the ImageViews by moving the images from one image view to another, loading new images only for the tiles that need it. Obviously, images that are no longer referenced by any image view will be eligible for garbage collection in the usual way.

    There are probably other ways to achieve this, such as using WritableImages and using a PixelWriter to update the pixel data when needed. Which works best probably depends somewhat on which is most convenient for the actual format you have for the image data; there is probably little performance difference between different strategies.

    If you are loading the images from a server or database, you should do so in the background. If the image is loaded from a URL, the Image class provides functionality to do this directly. If you are loading from an input stream (e.g. from a database BLOB field), you will need to implement the background threading yourself.

    Here is the basic idea (no threading):

    import java.util.Random;
    
    import javafx.application.Application;
    import javafx.beans.property.DoubleProperty;
    import javafx.beans.property.IntegerProperty;
    import javafx.beans.property.SimpleDoubleProperty;
    import javafx.beans.property.SimpleIntegerProperty;
    import javafx.geometry.Insets;
    import javafx.geometry.Pos;
    import javafx.scene.Scene;
    import javafx.scene.control.Label;
    import javafx.scene.image.Image;
    import javafx.scene.image.ImageView;
    import javafx.scene.layout.Background;
    import javafx.scene.layout.BackgroundFill;
    import javafx.scene.layout.CornerRadii;
    import javafx.scene.layout.Pane;
    import javafx.scene.paint.Color;
    import javafx.stage.Stage;
    
    public class PanningTilesExample extends Application {
    
        private static final int TILE_WIDTH = 100;
        private static final int TILE_HEIGHT = 100;
    
        private static final int PANE_WIDTH = 800;
        private static final int PANE_HEIGHT = 800;
    
        // amount scrolled left, in pixels:
        private final DoubleProperty xOffset = new SimpleDoubleProperty();
        // amount scrolled right, in pixels:
        private final DoubleProperty yOffset = new SimpleDoubleProperty();
    
        // number of whole tiles shifted to left:
        private final IntegerProperty tileXOffset = new SimpleIntegerProperty();
        // number of whole tiles shifted up:
        private final IntegerProperty tileYOffset = new SimpleIntegerProperty();
    
        private final Pane pane = new Pane();
    
        // for enabling dragging:
        private double mouseAnchorX;
        private double mouseAnchorY;
    
        // array of ImageViews:
        private ImageView[][] tiles;
    
        private final Random rng = new Random();
    
        @Override
        public void start(Stage primaryStage) {
    
            // update number of tiles offset when number of pixels offset changes:
            tileXOffset.bind(xOffset.divide(TILE_WIDTH));
            tileYOffset.bind(yOffset.divide(TILE_HEIGHT));
    
            // create the images views, etc. This method could be called
            // when the pane size changes, if you want a resizable pane with fixed size tiles:
            build();
    
            // while tile offsets change, allocate new images to existing image views:
    
            tileXOffset.addListener(
                    (obs, oldOffset, newOffset) -> rotateHorizontal(oldOffset.intValue() - newOffset.intValue()));
    
            tileYOffset.addListener(
                    (obs, oldOffset, newOffset) -> rotateVertical(oldOffset.intValue() - newOffset.intValue()));
    
            // Simple example just has a fixed size pane: 
            pane.setMinSize(PANE_WIDTH, PANE_HEIGHT);
            pane.setPrefSize(PANE_WIDTH, PANE_HEIGHT);
            pane.setMaxSize(PANE_WIDTH, PANE_HEIGHT);
    
    
            // enable panning on pane (just update offsets when dragging):
    
            pane.setOnMousePressed(e -> {
                mouseAnchorX = e.getSceneX();
                mouseAnchorY = e.getSceneY();
            });
    
            pane.setOnMouseDragged(e -> {
                double deltaX = e.getSceneX() - mouseAnchorX;
                double deltaY = e.getSceneY() - mouseAnchorY;
                xOffset.set(xOffset.get() + deltaX);
                yOffset.set(yOffset.get() + deltaY);
                mouseAnchorX = e.getSceneX();
                mouseAnchorY = e.getSceneY();
            });
    
            // display in stage:
            Scene scene = new Scene(pane);
            primaryStage.setScene(scene);
            primaryStage.show();
        }
    
        private void build() {
    
            // create array of image views:
    
            int numTileCols = (int) (PANE_WIDTH / TILE_WIDTH + 2);
            int numTileRows = (int) (PANE_HEIGHT / TILE_HEIGHT + 2);
    
            tiles = new ImageView[numTileCols][numTileRows];
    
            // populate array:
    
            for (int colIndex = 0; colIndex < numTileCols; colIndex++) {
    
                final int col = colIndex;
    
                for (int rowIndex = 0; rowIndex < numTileRows; rowIndex++) {
    
                    final int row = rowIndex;
    
                    // create actual image view and initialize image:                
                    ImageView tile = new ImageView();
                    tile.setImage(getImage(col - tileXOffset.get(), row - tileYOffset.get()));
                    tile.setFitWidth(TILE_WIDTH);
                    tile.setFitHeight(TILE_HEIGHT);
    
                    // position image by offset, and register listeners to keep it updated:
                    xOffset.addListener((obs, oldOffset, newOffset) -> {
                        double offset = newOffset.intValue() % TILE_WIDTH + (col - 1) * TILE_WIDTH;
                        tile.setLayoutX(offset);
                    });
                    tile.setLayoutX(xOffset.intValue() % TILE_WIDTH + (col - 1) * TILE_WIDTH);
    
                    yOffset.addListener((obs, oldOffset, newOffset) -> {
                        double offset = newOffset.intValue() % TILE_HEIGHT + (row - 1) * TILE_HEIGHT;
                        tile.setLayoutY(offset);
                    });
                    tile.setLayoutY(yOffset.intValue() % TILE_HEIGHT + (row - 1) * TILE_HEIGHT);
    
                    // add image view to pane:
                    pane.getChildren().add(tile);
    
                    // store image view in array:
                    tiles[col][row] = tile;
                }
            }
        }
    
        // tiles have been shifted off-screen in vertical direction
        // need to reallocate images to image views, and get new images
        // for tiles that have moved into view:
    
        // delta represents the number of tiles we have shifted, positive for up
        private void rotateVertical(int delta) {
    
            for (int colIndex = 0; colIndex < tiles.length; colIndex++) {
    
    
                if (delta > 0) {
    
                    // top delta rows have shifted off-screen
                    // shift top row images by delta
                    // add new images to bottom rows:
    
                    for (int rowIndex = 0; rowIndex + delta < tiles[colIndex].length; rowIndex++) {
    
                        // stop any background loading we no longer need
                        if (rowIndex < delta) {
                            Image current = tiles[colIndex][rowIndex].getImage();
                            if (current != null) {
                                current.cancel();
                            } 
                        }
    
                        // move image up from lower rows:
                        tiles[colIndex][rowIndex].setImage(tiles[colIndex][rowIndex + delta].getImage());
                    }
    
                    // fill lower rows with new images:
                    for (int rowIndex = tiles[colIndex].length - delta; rowIndex < tiles[colIndex].length; rowIndex++) {
                        tiles[colIndex][rowIndex].setImage(getImage(-tileXOffset.get() + colIndex, -tileYOffset.get() + rowIndex));
                    }
                }
    
                if (delta < 0) {
    
                    // similar to previous case...
                }
            }
    
        }
    
    
        // similarly, rotate images horizontally:
        private void rotateHorizontal(int delta) {
            // similar to rotateVertical....    
        }
    
        // get a new image for tile represented by column, row
        // this implementation just snapshots a label, but this could be
        // retrieved from a file, server, or database, etc
        private Image getImage(int column, int row) {
            Label label = new Label(String.format("Tile [%d,%d]", column, row));
            label.setPrefSize(TILE_WIDTH, TILE_HEIGHT);
            label.setMaxSize(TILE_WIDTH, TILE_HEIGHT);
            label.setAlignment(Pos.CENTER);
            label.setBackground(new Background(new BackgroundFill(randomColor(), CornerRadii.EMPTY , Insets.EMPTY)));
    
            // must add label to a scene for background to work:
            new Scene(label);
            return label.snapshot(null, null);
        }
    
    
        private Color randomColor() {
            return Color.rgb(rng.nextInt(256), rng.nextInt(256), rng.nextInt(256));
        }
    
        public static void main(String[] args) {
            launch(args);
        }
    }
    

    Complete code (with thread handling) here, complete version without threading in a previous revision

    There is obviously more functionality (and performance enhancements) that could be added here, for example you could allow for resizing the pane (update: the latest version of the gist linked above does this), and create or remove tiles when the pane changes size, etc. But this should function as a basic template for this functionality.

提交回复
热议问题