【问题标题】:How load javafx.scene.image.Image by demand?如何按需加载 javafx.scene.image.Image?
【发布时间】:2016-12-10 19:03:20
【问题描述】:

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

我可以让ImageView 仅在显示时加载它的图像吗?

【问题讨论】:

  • 不太清楚你在问什么。 Image 代表一组固定的图像数据,如果这就是您的意思。 WritableImage 代表一个固定大小的图像,但像素数据可以在实例化后通过其PixelWriter 进行更改。我不知道您所说的“我可以让 ImageView 仅在显示时加载它的图像吗?”。
  • @James_D 因为他们让Image 延迟加载。请参阅 cancel()isBackgroundLoading() 等方法。奇怪的是,这个过程只能朝着一个方向发展,就像在热力学定律中一样。
  • 嗯,不完全确定你的意思。但是,您的问题的解决方案是保留一组固定的ImageViews(足以平铺显示在任何滚动偏移值下)并根据需要更新它们的Image 属性。在图像视图上调用setImage() 将强制它重新绘制;如果您使用后台加载,它将在加载完成后重新绘制,这就是您想要的。明智的做法是在替换当前的Image 之前调用cancel(),以防止在后台进行不必要的工作。替换的图像将以通常的方式被 gc'd。
  • 您是否正在尝试创建类似于“谷歌地图”的界面?所以你有一些“瓦片”代表的视图,每个瓦片显示一个图像。用户可以“平移”(滚动),因此新曝光的图块必须加载新图像,而其他图像离开屏幕?
  • 是的,关于谷歌地图。还要考虑从 FXML 加载每个图块。还要考虑在显示部分加载图表时的任何其他情况(最终常见情况)

标签: java image javafx


【解决方案1】:

Image 类在其图像数据方面本质上是不可变的,因为您可以在构建时指定图像数据的来源,然后无法通过 API 对其进行修改。

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

实现“平铺图像”功能所需的基本策略是创建一个虚拟化容器,该容器具有一组“单元”或“平铺”,可重复使用以显示不同的内容。这实质上就是在 JavaFX 中实现 ListViewTableViewTreeView 等控件的方式。您可能还对 Tomas Mikula 的 Flowless 实现相同想法感兴趣。

因此,要实现“平铺图像”功能,您可以使用 ImageViews 数组作为“单元格”或“平铺块”。您可以将它们放在窗格中并在窗格中实现平移/滚动,当图像视图滚动到视图之外时,通过将图像从一个图像视图移动到另一个图像视图来重用ImageViews,仅为需要它。显然,不再被任何图像视图引用的图像将有资格以通常的方式进行垃圾回收。

可能还有其他方法可以实现这一点,例如使用WritableImages 和使用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 的最新版本可以做到这一点),并在窗格更改大小等。但这应该作为此功能的基本模板。

【讨论】:

  • 定期更新要点,我建议浏览一些revisions
【解决方案2】:

最好在图片显示之前加载它们!

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

如果您要重复使用该图像,请记住它! 加载一次并在无限的 imageViews 上使用它!

【讨论】:

    【解决方案3】:

    不,Image 合约中没有这样的功能。图片可以在后台加载,但是一旦加载就无法卸载。

    如果使用ImageView,那么您应该明确地将Image 分配给它,但是JavaFX 没有提供让您知道何时实际显示ImageView 的方法。

    为了实现接近ImageView 的要求,我将它分叉并高度利用带有Prism 的已弃用API,包括NGImageView 类。

    【讨论】:

    • 但是您可以使用ImageView 来做同样的事情。
    • 如果你知道然后回答。不要玩捉迷藏。
    • 正在处理它...这不是微不足道的,需要一些时间来写。
    • 不是像我一样写你自己的ImageView,或者更琐碎?
    • 没有必要这样做。只需(基本上)自己管理布局,以便在图像视图出现时加载新图像(如果需要)。请参阅我的答案(以及 github 示例,因为完整示例有点长)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-04-01
    • 1970-01-01
    • 2012-01-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多