Reputation: 2961
I'm simulating a compass where I need to display current angular position upon reception of angular data from another source(via network). But somehow the circle group is getting shifted upon applying transformation to Text node based upon angular position. This is a sample code
MainApp.java
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.scene.text.Text;
import javafx.scene.transform.Rotate;
import javafx.stage.Screen;
import javafx.stage.Stage;
public class MainApp extends Application {
static BorderPane borderPane;
static Group clock;
static Group angStatus;
public static Circle clockCircle;
public static Line angHand;
public static Text angText;
public static Rotate angRotate;
public static Label angLabel;
public static void main(String[] args) {
borderPane = new BorderPane();
clock = new Group();
angStatus = new Group();
clockCircle = new Circle();
clockCircle.centerXProperty().bind(borderPane.layoutXProperty());
clockCircle.centerYProperty().bind(borderPane.layoutYProperty());
clockCircle.setRadius(360);
clockCircle.setFill(Color.RED);
angHand = new Line();
angHand.startXProperty().bind(clockCircle.centerXProperty());
angHand.startYProperty().bind(clockCircle.centerYProperty());
angText = new Text();
angRotate = new Rotate();
angRotate.pivotXProperty().bind(angText.xProperty());
angRotate.pivotYProperty().bind(angText.yProperty());
angText.getTransforms().addAll(angRotate);
angLabel = new Label();
angLabel.layoutXProperty().bind(borderPane.layoutXProperty());
angLabel.layoutYProperty().bind(borderPane.layoutYProperty());
clock.getChildren().addAll(clockCircle, angHand, angText);
angStatus.getChildren().addAll(angLabel);
borderPane.setCenter(clock);
borderPane.setBottom(angStatus);
DataReceiver objDataReceiver = new DataReceiver();
Thread dataRecvThread = new Thread(objDataReceiver, "DATARECVR");
dataRecvThread.start();
launch(args);
}
@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("CLOCK");
primaryStage.setMaximized(true);
primaryStage.setScene(new Scene(borderPane, Screen.getPrimary().getVisualBounds().getWidth(), Screen.getPrimary().getVisualBounds().getHeight()));
primaryStage.show();
}
}
DataReceiver.java
import javafx.application.Platform;
import java.time.Instant;
import static java.lang.Thread.sleep;
public class DataReceiver implements Runnable {
public static int deg;
DataReceiver() {
deg = 0;
}
public void run() {
while(true) {
long startTime = Instant.now().toEpochMilli();
deg++;
while(deg >= 360)
deg -= 360;
while(deg < 0)
deg += 360;
long endTime = Instant.now().toEpochMilli();
Platform.runLater(() -> {
MainApp.angHand.endXProperty().bind(MainApp.clockCircle.radiusProperty().multiply(Math.sin(Math.toRadians(deg))));
MainApp.angHand.endYProperty().bind(MainApp.clockCircle.radiusProperty().multiply(-Math.cos(Math.toRadians(deg))));
MainApp.angText.xProperty().bind(MainApp.angHand.endXProperty().add(10 * Math.sin(Math.toRadians(deg))));
MainApp.angText.yProperty().bind(MainApp.angHand.endYProperty().subtract(10 * Math.cos(Math.toRadians(deg))));
MainApp.angText.setText(String.valueOf(deg));
MainApp.angRotate.setAngle(deg);
MainApp.angLabel.setText("Angle: " + deg);
});
try {
sleep(1000 - (endTime - startTime));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
I'm using jdk-13.0.1 and javafx-sdk-11.0.2
Upvotes: 1
Views: 527
Reputation: 82461
The problem here is the fact that Group
itself is non-resizeable, but changes it's own size to fit the contents. Modifying the transformations of the text results in the bounds of the Group
changing horizontally and/or vertically which in turn makes the parent layout reposition the group to keep it centered. Depending on your needs you may want wrap the clock in a parent not doing this (Pane
). If you want to keep the center of the circle centered, you'll probably need to implement your own layout.
Notes:
Thread
. Instead do this using AnimationTimer
or similar utilities provided by JavaFX.%
) which could deal with the logic of one of the while
loops. Furthermore the second loop is not actually needed, since the value of deg
is never decreased below 0.main
method. The way you implement the logic could result in an exception, if Platform.runLater
is called before the toolkit has been started. Better initialize this kind of logic from Application.start
.static
should be avoided, if possible, since it makes controlling the flow of data more complicated. And what if you want to display 2 clocks for different timezones? There's no way of reusing the class, if it relies on static
data in the way your classes do.A binding like the following makes no sense: borderPane
is the root of the scene and therefore will keep the position (0,0); furthermore angLabel
is a descendant of the borderPane
. I recommend not wrapping the label in a group any use the static
BorderPane.alignment
property to tell borderPane
how to position the node.
angLabel.layoutXProperty().bind(borderPane.layoutXProperty());
angLabel.layoutYProperty().bind(borderPane.layoutYProperty());
The following example makes Clock
a Control
with a property of type LocalTime
and calculates the positions of the children itself:
public class Clock extends Control {
private final ObjectProperty<LocalTime> time = new SimpleObjectProperty<>(this, "time", LocalTime.MIDNIGHT) {
@Override
public void set(LocalTime newValue) {
// make sure the value is non-null
super.set(newValue == null ? LocalTime.MIDNIGHT : newValue);
}
};
public ObjectProperty<LocalTime> timeProperty() {
return time;
}
public LocalTime getTime() {
return time.get();
}
public void setTime(LocalTime value) {
time.set(value);
}
@Override
protected Skin<?> createDefaultSkin() {
return new ClockSkin(this);
}
}
public class ClockSkin extends SkinBase<Clock> {
private static final double SPACE = 20;
private static final double FACTOR = 360d / 60;
private final Circle face;
private final Line secondsHand;
private final Rotate rotate;
private final Text secondsText;
public ClockSkin(Clock control) {
super(control);
face = new Circle(360, Color.RED);
// line straight up from center to circle border
secondsHand = new Line();
secondsHand.endXProperty().bind(face.centerXProperty());
secondsHand.endYProperty().bind(face.centerYProperty().subtract(face.getRadius()));
secondsHand.startXProperty().bind(face.centerXProperty());
secondsHand.startYProperty().bind(face.centerYProperty());
secondsText = new Text();
rotate = new Rotate();
rotate.pivotXProperty().bind(face.centerXProperty());
rotate.pivotYProperty().bind(face.centerYProperty());
secondsHand.getTransforms().add(rotate);
secondsText.getTransforms().add(rotate);
registerChangeListener(control.timeProperty(), (observable) -> {
LocalTime value = (LocalTime) observable.getValue();
update(value);
control.requestLayout();
});
getChildren().addAll(face, secondsHand, secondsText);
update(control.getTime());
}
protected void update(LocalTime time) {
int seconds = time.getSecond();
secondsText.setText(Integer.toString(seconds));
rotate.setAngle(seconds * FACTOR);
}
@Override
protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) {
// center face
face.setCenterX(contentX + SPACE + contentWidth / 2);
face.setCenterY(contentY + SPACE + contentHeight / 2);
// position text
secondsText.setX(contentX + SPACE + (contentWidth - secondsText.prefWidth(-1)) / 2);
secondsText.setY(face.getCenterY() - face.getRadius() - SPACE / 2);
}
@Override
protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset,
double leftInset) {
return 2 * (SPACE + face.getRadius()) + leftInset + rightInset;
}
@Override
protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset,
double leftInset) {
return 2 * (SPACE + face.getRadius()) + topInset + bottomInset;
}
@Override
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset,
double leftInset) {
return computeMinWidth(height, topInset, rightInset, bottomInset, leftInset);
}
@Override
protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset,
double leftInset) {
return computeMinHeight(width, topInset, rightInset, bottomInset, leftInset);
}
@Override
public void dispose() {
getChildren().clear();
super.dispose();
}
}
@Override
public void start(Stage primaryStage) throws Exception {
BorderPane borderPane = new BorderPane();
Clock clock = new Clock();
clock.setTime(LocalTime.now());
Label angLabel = new Label();
BorderPane.setAlignment(angLabel, Pos.TOP_LEFT);
borderPane.setCenter(clock);
borderPane.setBottom(angLabel);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss");
AnimationTimer timer = new AnimationTimer() {
@Override
public void handle(long now) {
LocalTime time = LocalTime.now();
clock.setTime(time);
angLabel.setText(formatter.format(time));
}
};
timer.start();
primaryStage.setTitle("CLOCK");
primaryStage.setMaximized(true);
primaryStage.setScene(new Scene(borderPane));
primaryStage.show();
}
Note that here the separation between the logic for updating the visuals and updating the data is done much more cleanly than in your code. In general you want to prevent access of other classes to internal logic, since this prevents outside interference that could possibly break your control.
Upvotes: 2