Image 类在其图像数据方面本质上是不可变的,因为您可以在构建时指定图像数据的来源,然后无法通过 API 对其进行修改。
ImageView 类提供了在 UI 中显示图像的功能。 ImageView 类是可变的,您可以更改它显示的图像。
实现“平铺图像”功能所需的基本策略是创建一个虚拟化容器,该容器具有一组“单元”或“平铺”,可重复使用以显示不同的内容。这实质上就是在 JavaFX 中实现 ListView、TableView 和 TreeView 等控件的方式。您可能还对 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 的最新版本可以做到这一点),并在窗格更改大小等。但这应该作为此功能的基本模板。