如何按需加载javafx.scene.image.Image?

时间:2016-08-04 15:04:04

标签: java image javafx

是否可以丢弃Image的已加载内容,然后再次加载?是否可以按需加载?

我可以ImageView只在show上加载它的图像吗?

3 个答案:

答案 0 :(得分:1)

Image类在其图像数据方面基本上是不可变的,因为您可以在构造时指定图像数据的源,然后不能通过API随后修改它。

ImageView类提供在UI中显示图像的功能。 ImageView类是可变的,因为您可以更改它显示的图像。

您需要实施的基本策略"平铺图像"功能是创建一个虚拟化容器,它有一个" cell"或"瓷砖"它们被重用来显示不同的内容。这实际上是在JavaFX中实现ListViewTableViewTreeView等控件的方式。您可能也对Tomas Mikula Flowless实现同样的想法感兴趣。

所以要实现"平铺图像"功能,您可以使用ImageView s数组作为"单元格"或"瓷砖"。您可以将它们放在窗格中并在窗格中实现平移/滚动,当图像视图滚出视图时,通过将图像从一个图像视图移动到另一个图像视图来重用ImageView,仅为图像视图加载新图像需要它的瓷砖。显然,任何图像视图不再引用的图像都有资格以通常的方式进行垃圾收集。

可能还有其他方法可以实现此目的,例如使用WritableImage并使用PixelWriter在需要时更新像素数据。最好的方法可能取决于哪种方式对于图像数据的实际格式最为方便;不同的策略之间可能没有什么性能差异。

如果要从服务器或数据库加载图像,则应在后台执行此操作。如果从URL加载图像,则Image类提供直接执行此操作的功能。如果从输入流(例如,从数据库BLOB字段)加载,则需要自己实现背景线程。

这是基本想法(没有线程):

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);
    }
}

完整代码(带有线程处理)here,完整版本没有previous revision中的线程

显然可以添加更多功能(和性能增强),例如,您可以允许调整窗格大小(更新:上面链接的gist的最新版本执行此操作),并在创建或删除切片时窗格更改大小等。但这应该作为此功能的基本模板。

答案 1 :(得分:0)

最佳做法是在显示图像之前加载图像!

如果您想摆脱图像,只需将图像设置为null即可!但是,您将重新初始化该图像,以便能够查看!我不推荐这个!

如果您将重复使用该图像,请保留内存! 加载一次并在无限的imageViews上使用它!

答案 2 :(得分:-1)

不,Image合同中没有此类功能。图像可以在后台加载,但一旦加载,就无法卸载。

如果使用ImageView,那么您应该明确地为其分配Image,但JavaFX并未提供一种方法让您知道实际显示ImageView的时间。

为了实现所需的接近ImageView,我应该使用Prism来高度利用已弃用的API,包括NGImageView类。