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

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?

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.

It is best practice to have your images loaded before they are displayed!
If you wish to get rid of the image simply set the image to null! But you will then have ro reinitialize that image in order to be able to view! I do not recommend this!
If you will reuse that image just keep it memory!
Load it once and use it on unlimited imageViews!

No, there is no such functionality in Image contract. Image can load in background, but once loaded, it cannot be unloaded.
If using ImageView, then you should assign Image to it explicitly, but JavaFX doesn't provide a way for you to know when ImageView is actually shown.
To implement required close to ImageView, I was to fork it and highly utilize deprecated API with Prism, including NGImageView class.

Related

Does JavaFX seem to be losing my event handler?

I'm a complete novice to JavaFX, and fairly new to Java overall. I'm designing a graphical representation of an undirected graph for use in a self-teaching project. Right now, I'm trying to make nodes draggable such that the edges will stretch to stay connected with their nodes. I have achieved that in the case of 2 nodes with a connection. However, adding a third does something weird.
Say we have this situation:
Cell testOne = new Cell ("testOne", 123);
Cell testTwo = new Cell ("testTwo", 456);
Cell testThree = new Cell ("testThree", 200);
testOne.addConnection(testTwo);
testOne.addConnection(testThree);
What I get is three nodes with two lines strewn randomly in their general area (worth noting the nodes are positioned in a crudely random way). If I move around testTwo or testThree, a single line will trade off being connected to testOne. The second line remains unchanged no matter what. I have to think that somehow what's happening is that one of the EventHandlers is getting "unplugged" from their respective cells, or else somehow one of the lines is getting lost in memory. Here's the code to draw lines (I know it's really clunky). This method is in the Graph class, which controls graphic (oop) representation of the class. "cells" is the ArrayList storing all its nodes, "connections" is the arrayList in the Cell instance that keeps track of all the nodes it's connected to, and "LinesBetween" is a HashMap the Cell instance keeping track of whether a line has already been drawn between the two nodes.
public void drawAndManageEdgeLines(){
if (cells.size() > 1) { //don't wanna make connections if there's only one cell, or none
int count = 0;
for (Cell cell : cells) { // for every cell on the graph
List<Cell> connectionsList = cell.getConnections(); // look at that cell's connections
if (!connectionsList.isEmpty()) {// validate that the cell is actually supposed to be connected to something
for (Cell connection : connectionsList) { // go through all their connections
if (!cell.getLinesBetween().get(connection) && cell.getLinesBetween().get(connection) != null) { //check to see whether there is already a line between them
Circle sourceCircle = cell.getCellView();
Circle targetCircle = connection.getCellView();
Bounds sourceBound = sourceCircle.localToScene(sourceCircle.getBoundsInLocal());
Bounds targetBound = targetCircle.localToScene(targetCircle.getBoundsInLocal());
double targetX = targetBound.getCenterX();
double targetY = targetBound.getCenterY();
double sourceX = sourceBound.getCenterX();
double sourceY = sourceBound.getCenterY();
edge = new Line(sourceX, sourceY, targetX, targetY);
edge.setStroke(Color.BLACK);
edge.setStrokeWidth(2);
getChildren().add(edge);
edge.toBack();
cell.setLinesBetweenEntry(connection, true);
connection.setLinesBetweenEntry(cell, true);
// these handlers control where the line is dragged to
cell.addEventHandler(MouseEvent.MOUSE_DRAGGED, new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent e) {
edge.setStartX(e.getSceneX()); //this is a really cool method
edge.setStartY(e.getSceneY());
e.consume();
}
});
System.out.println("on round " + count + " we got there: ");
connection.addEventHandler(MouseEvent.MOUSE_DRAGGED, new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent e) {
edge.setEndX(e.getSceneX());
edge.setEndY(e.getSceneY());
e.consume();
}
});
}
}
}
}
}
}
It's hard to tell what's going wrong without a proper minimal reproducible example, but you seem to be making this more complicated than it needs to be. If you want the edges to be "linked" to the nodes then I recommend you use bindings. Here's a proof-of-concept:
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.scene.Cursor;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
Circle node1 = new Circle(100, 100, 50, Color.FIREBRICK);
Circle node2 = new Circle(900, 550, 50, Color.DARKBLUE);
Circle node3 = new Circle(900, 100, 50, Color.DARKGREEN);
addDragHandling(node1);
addDragHandling(node2);
addDragHandling(node3);
Line edge1 = createEdge(node1, node2);
Line edge2 = createEdge(node1, node3);
Pane root = new Pane(edge1, edge2, node1, node2, node3);
primaryStage.setScene(new Scene(root, 1000, 650));
primaryStage.show();
}
private Line createEdge(Circle from, Circle to) {
Line edge = new Line();
edge.setStrokeWidth(2);
edge.startXProperty().bind(from.centerXProperty());
edge.startYProperty().bind(from.centerYProperty());
edge.endXProperty().bind(to.centerXProperty());
edge.endYProperty().bind(to.centerYProperty());
return edge;
}
private void addDragHandling(Circle circle) {
// changing cursors not necessary, only included to help indicate
// when something can be dragged and is being dragged
circle
.cursorProperty()
.bind(
Bindings.when(circle.pressedProperty())
.then(Cursor.CLOSED_HAND)
.otherwise(Cursor.OPEN_HAND));
double[] offset = {0, 0}; // (x, y)
circle.setOnMousePressed(
e -> {
offset[0] = e.getX() - circle.getCenterX();
offset[1] = e.getY() - circle.getCenterY();
e.consume();
});
circle.setOnMouseDragged(
e -> {
circle.setCenterX(e.getX() - offset[0]);
circle.setCenterY(e.getY() - offset[1]);
e.consume();
});
}
}
Note I added the edges to the Pane first so that they were drawn under the nodes. See Z-Order in JavaFX. Also, your drag logic may look different depending on what Node you use to represent your graph nodes.
Since you are representing a graph your application will be more complex. If the graph is dynamic and you want the view to update in real time then you'll need to keep references to the nodes and their associated edges to add and remove them at will. But remember the view is only a visual representation of the model. Don't use the view to store model information (e.g. what nodes and edges actually exist).

JavaFX How Can I Center a Tooltip on a Node?

I'm currently working on a form that features validation upon changing focus from one node to another and wish to display a tooltip centered above a node containing an error if one exists. I'm using the tooltip's show(Node ownerNode, double anchorX, double anchorY) method signature to specify which node I would like to attach it to and where to position it.
I've tried the following code:
Tooltip tooltip = new Tooltip("Error");
tooltip.show(node, 0, 0);
double tooltipMiddle = tooltip.getWidth() / 2;
tooltip.hide();
double nodeMiddle = node.getWidth() / 2;
//Gets the node's x and y position within the window
Bounds bounds = node.localToScene(node.getBoundsInLocal());
//Tooltip's bottom-right corner is set to the anchor points, so I set x to
//the node's x coordinate plus half the width of the node plus half the width
//of the tooltip
tooltip.show(node,
bounds.getMinX() + nodeMiddle + tooltipMiddle, bounds.getMinY());
This has gotten me very close to the center, but it's still off. I've been all over the internet trying to find help, but I'm just not finding any, so I figured I'd ask here.
Any chance I could get some insight into why I'm not able to get this working how I'd like it?
Code which I bring provides correct positioning of tooltip but is far from being perfect. It would take a lot of work to bring comprehensive solution (if you want we can discuss it).
Going to the bottom I think you have a math problem and Tooltip class may not be the best option.
import javafx.application.Application;
import javafx.geometry.Bounds;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class TooltipApp extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) throws Exception {
TextField textField = new TextField();
textField.setPrefWidth(100.);
Button button = new Button("Tooltip");
HBox hBox = new HBox(textField, button);
hBox.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
Label label = new Label("Empty!");
label.setVisible(false);
Pane tooltipPane = new Pane(label);
tooltipPane.setMouseTransparent(true);
StackPane stackPane = new StackPane(hBox, tooltipPane);
stackPane.setPrefSize(600., 400.);
Scene scene = new Scene(stackPane);
stage.setScene(scene);
stage.show();
button.setOnMouseClicked(event -> {
if (label.isVisible()) {
label.setVisible(false);
} else {
Bounds sceneBounds = textField.localToScene(textField.getBoundsInLocal());
label.setLayoutX((sceneBounds.getMinX() + (sceneBounds.getWidth() / 2)) - (label.getWidth() / 2));
label.setLayoutY(sceneBounds.getMinY() - label.getHeight());
label.setVisible(true);
}
});
}
}
First you should use Bounds bounds = node.localToScreen(node.getBoundsInLocal()); as it might not be clear what the relevant Scene is.
Then calling tooltip.show(node, bounds.getMinX(), bounds.getMinY()); should give you a tooltip with an upper left corner identical to the upper upper left corner of node.
Now, tooltip.show(node, bounds.getMinX() + nodeMiddle - tooltipMiddle, bounds.getMinY() - tooltipHeight); should produce the desired result and it does for
double tooltipMiddle = (tooltip.getWidth()-18) / 2;
double tooltipHeight = tooltip.getHeight()-18;
double nodeMiddle = getWidth() / 2;
The 18 came from a little experimenting and I'm not sure why the width and height are off by that amount, but it seems to be indpendent of the length or number of lines of the text in the tooltip.

Get GridPane consisting of ImageViews to fit in Margin of BorderPane and resizing

I have a question concerning a Uni-project I'm working on.
We have to create a game, where we have a board that consists of tiles and you should be able to rotate these tiles with clicks. The Tiles should always fit in the Grid and the Grid should resize with the Window.
Now I made a class "TileView" which extends ImageView to get the pictures matching the pattern of the tile. Then I have TileGrid, which extends GridPane, which should consist of width*height tileviews. And this GridPane is in a BorderPane and this makes the scene.
Unfortunately I can't really put Code in here, because we have strict rules of plagiarism (and if someone sees my code and copies it, I'm going down too).
So, I make 2 for loops for the width and height and create a new TileView and set PreserveRatio on true and then do this:
setRowIndex(temp, i);
setColumnIndex(temp, j);
super.getChildren().add(temp);
this is in the class TileGrid.
Then I add it to the BorderPane with margin insets 100 and this is what happens:
https://imgur.com/a/OYSwER2
https://imgur.com/a/YUsZX09
But now I have the problem of fitting the TileViews to the size of the grid and make them resize with the window. I tried ("temp" is the just created ImageView as I put them in the TileGrid):
temp.fitWidthProperty().bind(prefWidthProperty());
temp.fitHeightProperty().bind(prefHeightProperty());
but then my single ImageViews are gigantic.
https://imgur.com/a/zaCz3OB
This is filling my whole screen.
I've tried numerous things, searched for hours and never achieved what I wanted so I really hope someone can help me or give me a tip even without my code.
Thanks in advance!
Here is a MCVE that demos what you need.
Centers the GridPane content
centerRoot.setAlignment(Pos.CENTER);
Binds the ImageViews fitHeight and fitWidth properties with the GridPanes height and width properties - 100(due to setMargins) / 10(the number of rows/columns)
for (int i = 0; i < 10; i++) {
for (int t = 0; t < 10; t++) {
Image image = new Image("https://s3.amazonaws.com/media.eremedia.com/uploads/2012/08/24111405/stackoverflow-logo-700x467.png");
ImageView imageView = new ImageView(image);
imageView.fitHeightProperty().bind(centerRoot.heightProperty().subtract(100).divide(10));
imageView.fitWidthProperty().bind(centerRoot.widthProperty().subtract(100).divide(10));
imageView.setPreserveRatio(false);
centerRoot.add(imageView, i, t);
}
}
Full Code:
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
/**
*
* #author blj0011
*/
public class JavaFXApplication241 extends Application
{
#Override
public void start(Stage primaryStage)
{
BorderPane root = new BorderPane();
GridPane centerRoot = new GridPane();
centerRoot.setAlignment(Pos.CENTER);
GridPane.setMargin(centerRoot, new Insets(50, 50, 50, 50));
root.setCenter(centerRoot);
Scene scene = new Scene(root, 500, 500);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
for (int i = 0; i < 10; i++) {
for (int t = 0; t < 10; t++) {
Image image = new Image("https://s3.amazonaws.com/media.eremedia.com/uploads/2012/08/24111405/stackoverflow-logo-700x467.png");
ImageView imageView = new ImageView(image);
imageView.fitHeightProperty().bind(centerRoot.heightProperty().subtract(100).divide(10));
imageView.fitWidthProperty().bind(centerRoot.widthProperty().subtract(100).divide(10));
imageView.setPreserveRatio(false);
centerRoot.add(imageView, i, t);
}
}
}
/**
* #param args the command line arguments
*/
public static void main(String[] args)
{
launch(args);
}
}
As you can't provide any code I'll try to give you steps to work with I hope it might help
First as the BorderPane and GridPane will adjust their sizes to the window you have one problem which is adjusting the sizes of the ImageViews to make it resizable you have to bind their sizes to the size of the scene and take into consideration the number of the rows and columns you wanted for the grid
let's say you have 2 columns so image is 50% of the scene
imageView.fitWidthProperty().bind(scene.getWidthProperty().divide(2));
Note: I'm not sure about the syntax of this line I kinda busy sorry

JavaFX rectangle mouseonclick grid return

I am writing a board game which has a 20x20 grid.
This is in my board class:
private final Position[][] grid = new Position[GRID_SIZE][GRID_SIZE];
each position has :
public class Position {
private final Coordinates pos;
private Player player;
private final static double RECTANGLE_SIZE = 40.0;
private final Rectangle rect = new Rectangle(RECTANGLE_SIZE, RECTANGLE_SIZE);
}
so basically I have 20x20 Positions and each positions has a Rectangle
This is what I do to display the grid
for (int cols = 0; cols < GRID_SIZE; ++cols) {
for (int rows = 0; rows < GRID_SIZE; ++rows) {
grid.add(gameEngine.getBoard().getGrid()[cols][rows].getRect(), cols, rows);
}
}
Anyway, the grid is initialized and works properly. What I want to do is to make the rectangle objects clickable and to be able to return their Coordinates when they are clicked.
This is how I handle the mouse click
private void setUpRectangle() {
rect.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
rect.setFill(Color.BLACK);
}
});
}
What this code does is to change the color of the rectangle to black, but how could I return the Coordinates.
Basically, I can edit the onclick function to return the coordinates of this position, but how can I acquire them later?
This is not a JavaFX question as much as it is a design question. You have a container (Position) of 2 objects (Coordinates and Rectangle) and you want one of them to know about the other. That is, the rectangle should know the coordinates of its position.
There are a few approaches here, and depending on the bigger picture, one might be better than the others. James_D mentioned a couple in a comment.
Keep a reference of the position object in the rectangle object. This is useful if rectangle needs to access various datum in the container from various places. You would do something like rectangle.getPosition().getCoordinates() or .getPlayer().
Keep a reference of the coordinates object in the rectangle object. This is a more specific approach of 1 useful if you only need that object. You would do something like rectangle.getCoordinates().
Pass the coordinates to your setUpRectangle method. This is useful if you rectangle doesn't need access to this data from various places, it's a local solution. Then in the handle method you would return the coordinates you passed to setUpRectangle, though we can't see what class this method is in.
Use external help. You can keep something like Map<Rectangle, Coordinates> and then call map.get(rectangle). You can hide this map in a method Coordinates getCoordinatesForRectangle(Rectangle rectangle) instead of calling it directly.
You could store this data as userData (or use properties in case userData is preserved for something else in your program):
private final Rectangle rect;
public Position() {
rect = new Rectangle(RECTANGLE_SIZE, RECTANGLE_SIZE);
rect.setUserData(this);
}
rect.setOnMouseClicked((MouseEvent event) -> {
Position pos = (Position) ((Node) event.getSource()).getUserData();
...
});
You could also use a listener that knows about the position:
class CoordinateAwareListener implements EventHandler<MouseEvent> {
private final int coordinateX;
private final int coordinateY;
public CoordinateAwareListener(int coordinateX, int coordinateY) {
this.coordinateX = coordinateX;
this.coordinateY = coordinateY;
}
#Override
public void handle(MouseEvent event) {
// do something with the coordinates
}
}

Display text progressively with libgdx

I am looking for a way to display text progressively with libgdx, but I can't find a way to do it exactly the way I want. Here is what I did:
I have a text label that is being updated periodically to display a different text. The label is set to setWrap(true); and setAlignment(Align.center);
Every time I change the text of the label I use a custom Action which I built like this
public class DisplayTextAction extends TemporalAction{
private CharSequence completeText;
#Override
protected void update(float percent) {
((Label)actor).setText(
completeText.subSequence(
0,
(int)Math.round(completeText.length()*percent));
}
public void setText(String newText){
completeText = newText;
}
}
Every text update, I call the action from a pool, change the text and add the action to the label.
Here is my problem: This doesn't work the way I want with a centered and wrapped text.
This happens when text isn't centered (dots represent space):
|h........|
|hel......|
|hello....|
(Works as intended)
This is what happens when the text is centered:
|....h....|
|...hel...|
|..hello..|
And this is how I want it to behave:
|..h......|
|..hel....|
|..hello..|
My original idea to fix this was to use 2 sets of strings, one that is the visible text, and one invisible that acts as "padding". I came up with something like this:
CharSequence visibleText = completeText.subSequence(
0,
(int)Math.round(completeText.length()*percent));
CharSequence invisibleText = completeText.subSequence(
(int)Math.round(completeText.length()*percent),
completeText.length());
So I have my two sets of strings, but I can't find a way to display two different fonts (one visible, and another one which is the same but with an alpha of 0) or styles in the same label with Libgdx.
I'm stuck, I don't know if my approach is the right one or if I should look into something completely different, and if my approach is correct, I don't know how to follow it up using libgdx tools.
EDIT:
I followed Jyro117's instructions and I could make great progress, but I couldn't make it work with centred text on multiple lines.
imagine this text:
|all those lines are|
|..for a very long..|
|........text.......|
And it has to be displayed like this
|all th.............|
|...................|
|...................|
|all those line.....|
|...................|
|...................|
|all those lines are|
|..for a ve.........|
|...................|
|all those lines are|
|..for a very long..|
|........text.......|
Jyro117's solution give either
|all those lines are|
|for a very long....|
|text...............|
displayed correctly.
or
|...................|
|......all tho......|
|...................|
|...................|
|...all those lin...|
|...................|
|all those lines are|
|......for a v......|
|...................|
You are over-complicating the solution. All you really need is to determine the size of the label when all the text is added. Once you have determined that, lock the label size to those dimensions, put it inside of a table that expands to fill up the area around it, and then update your label with the action. (You can use a pool and such as needed, but for simplicity I left that out of the code below).
You will have to obviously adapt the code to yours, but this gives you a code reference to what I mean.
Here is a code snippet on one way to do it:
stage = new Stage(Gdx.graphics.getWidth(), Gdx.graphics.getHeight(), false);
Gdx.input.setInputProcessor(stage);
uiSkin = new Skin(Gdx.files.internal("skin/uiskin.json"));
Table fullScreenTable = new Table();
fullScreenTable.setFillParent(true);
final String message = "hello";
final Label progressLabel = new Label(message, this.uiSkin);
final TextBounds bounds = progressLabel.getTextBounds(); // Get libgdx to calc the bounds
final float width = bounds.width;
final float height = bounds.height;
progressLabel.setText(""); // clear the text since we want to fill it later
progressLabel.setAlignment(Align.CENTER | Align.TOP); // Center the text
Table progressTable = new Table();
progressTable.add(progressLabel).expand().size(width, height).pad(10);
final float duration = 3.0f;
final TextButton button = new TextButton("Go!", this.uiSkin);
button.addListener(new ClickListener() {
#Override public void clicked(InputEvent event, float x, float y) {
progressLabel.addAction(new TemporalAction(duration){
LabelFormatter formatter = new LabelFormatter(message);
#Override protected void update(float percent) {
progressLabel.setText(formatter.getText(percent));
}
});
}
});
stage.addActor(button);
fullScreenTable.add(progressTable);
fullScreenTable.row();
fullScreenTable.add(button);
stage.addActor(fullScreenTable);
Edit:
Added code to center and top align text in label. Also added code to fill spaces on the end to allow for proper alignment. Note: Only useful for mono-spaced fonts.
class LabelFormatter {
private final int textLength;
private final String[] data;
private final StringBuilder textBuilder;
LabelFormatter(String text) {
this.textBuilder = new StringBuilder();
this.data = text.split("\n");
int temp = 0;
for (int i = 0 ; i < data.length; i++) {
temp += data[i].length();
}
textLength = temp;
}
String getText(float percent) {
textBuilder.delete(0, textBuilder.length());
int current = Math.round(percent * textLength);
for (final String row : data) {
current -= row.length();
if (current >= 0) {
textBuilder.append(row);
if (current != 0) {
textBuilder.append('\n');
}
} else {
textBuilder.append(row.substring(0, row.length() + current));
// Back fill spaces for partial line
for (int i = 0; i < -current; i++) {
textBuilder.append(' ');
}
}
if (current <= 0) {
break;
}
}
return textBuilder.toString();
}
}

Categories

Resources