Reputation: 414
I have an app with lots of draggable Nodes, which can get a bit slow. In particular when:
Q: Is there anything to gain by doing some of the operations (the few for which I can control types) on integers (converting to ints first) and then re-assigning to doubles? (EDIT: this has now been answered)
Q1: How else could I speed it up?
==========UPDATE: Thanks to advice so far I determined my main problem is panning a Region container inside a Region root, as it triggers a layout pass in the container Region, which in turn (I think) ping-pongs to do a layout pass in all the (ca. 40) children Nodes (which are complex in their own right - they contain textfields, buttons, etc.).
So far the fastest solution I found is panning a Group in a Group (already a lot faster), where the Groups are additionally modified (subclass, override compute... methods) to make sure the parent/root will not need to be resized during dragging (sadly, not so noticeable).
class FastGroup extends Group {
//'stop asking about size' functionality
double widthCache;
double heightCache;
protected double computePrefWidth(double height) {
return widthCache != 0 ? widthCache : super.computePrefWidth(height);
}
protected double computePrefHeight(double width) {
return heightCache != 0 ? heightCache : super.computePrefHeight(width);
}
public void initDragging(){
//if a child of this Group goes into drag
//add max margins to Group's size
double newW = getBoundsInLocal().getWidth() + TestFXApp03.scene.getWidth()*2;
double newH = getBoundsInLocal().getHeight() + TestFXApp03.scene.getHeight()*2;
//cache size
widthCache = newW;
heightCache = newH;
}
public void endDragging(){
widthCache = 0;
heightCache = 0;
}
}
However Group creates problems of its own.
Q3: why can't I achieve the same with Pane (tried that as a 3rd option)? According to the Docs, a Pane:
'does not perform layout beyond resizing resizable children to their preferred size'
...while a Group:
'will "auto-size" its managed resizable children to their preferred sizes during the layout pass'
'is not directly resizable'
...which sounds quite the same to me, and yet the Pane used as root results in very slow panning of the contained Group.
Upvotes: 1
Views: 1392
Reputation: 1907
Ok... I spent some time in checking the performance of mouse-drags with many objects....
I did it on a Mac Pro (4 core, 8GB RAM).
If I implement the drag as image drag using the snapshot() function, the snapshot of 1.000.000 circles takes 600ms. Then the drag is smooth and fast.
Here the full code with and without using an image during drag:
public class DragMany extends Application {
Point2D lastXY = null;
Scene myScene;
long lastDrag;
ImageView dragImage;
void fill(Group box) {
Color colors[] = new Color[]{Color.RED, Color.BLUE, Color.GREEN, Color.YELLOW, Color.PINK, Color.MAGENTA };
for (int i = 0; i < 100000; i++) {
Circle c = new Circle(2);
box.getChildren().add(c);
c.setFill(colors[(i/17) % colors.length]);
c.setLayoutX(i % 30 * 3);
c.setLayoutY((i/20) % 30*3);
}
box.setLayoutX(40);
box.setLayoutY(40);
}
void drag1(Pane pane, Group box) {
box.setOnMousePressed(event -> {
lastXY = new Point2D(event.getSceneX(), event.getSceneY());
lastDrag = System.currentTimeMillis();
});
box.setOnMouseDragged(event -> {
event.setDragDetect(false);
Node on = box;
double dx = event.getSceneX() - lastXY.getX();
double dy = event.getSceneY() - lastXY.getY();
on.setLayoutX(on.getLayoutX()+dx);
on.setLayoutY(on.getLayoutY()+dy);
lastXY = new Point2D(event.getSceneX(), event.getSceneY());
event.consume();
});
box.setOnMouseReleased(d -> lastXY = null);
}
void drag3(Pane pane, Group box) {
box.setOnMousePressed(event -> {
long now = System.currentTimeMillis();
lastXY = new Point2D(event.getSceneX(), event.getSceneY());
SnapshotParameters params = new SnapshotParameters();
params.setFill(Color.TRANSPARENT);
WritableImage image = box.snapshot(params, null);
dragImage = new ImageView(image);
dragImage.setLayoutX(box.getLayoutX());
dragImage.setLayoutY(box.getLayoutY());
dragImage.setTranslateX(box.getTranslateX());
dragImage.setTranslateY(box.getTranslateY());
pane.getChildren().add(dragImage);
dragImage.setOpacity(0.5);
box.setVisible(false);
System.out.println("Snap "+(System.currentTimeMillis()-now)+"ms");
pane.setOnMouseDragged(e -> {
if (dragImage == null) return;
Node on = dragImage;
double dx = e.getSceneX() - lastXY.getX();
double dy = e.getSceneY() - lastXY.getY();
on.setTranslateX(on.getTranslateX()+dx);
on.setTranslateY(on.getTranslateY()+dy);
lastXY = new Point2D(e.getSceneX(), e.getSceneY());
e.consume();
});
pane.setOnMouseReleased(e -> {
if (dragImage != null) {
box.setTranslateX(dragImage.getTranslateX());
box.setTranslateY(dragImage.getTranslateY());
pane.getChildren().remove(dragImage);
box.setVisible(true);
dragImage = null;
}
lastXY = null;
e.consume();
});
lastDrag = System.currentTimeMillis();
event.consume();
});
}
public void start(Stage primaryStage) {
Pane mainPane = new Pane();
myScene = new Scene(mainPane, 500, 500);
primaryStage.setScene(myScene);
primaryStage.show();
Group all = new Group();
fill(all);
mainPane.getChildren().add(all);
drag3(mainPane, all);
}
public static void main(String[] args) {
launch(args);
}
Upvotes: 3
Reputation: 344
Using ints or floats instead of doubles won't give you much performance boost.
Instead, you should try optimizing when you change the position of nodes. For example, when you are dragging, instead of changing the position of all nodes, you could just render all the nodes that are being dragged at an offset and only change position of one node. Once you are done dragging, you could recalculate positions of all nodes you dragged.
Here is my guess of how you do it right now:
void drag(float x, float y) {
setPosition(x, y); // I'm gueessing this is where your bottle neck is
for (Node node : nodes) {
node.drag(x, y);
}
}
void render() {
screen.draw(this.x, this.y);
for (Node node : nodes) {
node.render();
}
}
Here is how I would optimize it by introducing render offset, so that you only set position of all the nodes when you are finished dragging:
float xo, yo; // render offsets
void drag(float x, float y) {
xo = x;
yo = y;
}
void dragEnd(float x, float y) {
setPosition(x, y);
for (Node node : nodes) {
node.dragEnd(x, y);
}
}
void render(float xo, float yo) {
xo += this.xo;
yo += this.yo;
screen.draw(this.x + xo, this.y + yo); // render with at offset
for (Node node : nodes) {
node.render(xo, yo);
}
}
Upvotes: 1