JavaFX - 调整简单应用程序的大小导致 "flickering" 由于 canvas clearing/repaint

JavaFX - resizing simple application causes "flickering" due to canvas clearing/repaint

我有一个非常简单的项目,它基本上在 JavaFX 场景 Canvas 的中间绘制一个黑色圆圈,并且每 50 毫秒增长一次。

这是我的控制器:

public class PrimaryController {

    public StackPane theRootPane;
    public CanvasPane theCanvasPane;
    int i = 1;

    public void initialize() {
        theCanvasPane  = new CanvasPane(500, 300);
        theRootPane.getChildren().addAll(theCanvasPane);

        Timeline theTimeline =
            new Timeline(new KeyFrame(Duration.millis(50), actionEvent -> UpdateCanvas(i++)));
        theTimeline.setCycleCount(Timeline.INDEFINITE);
        theTimeline.play();
    }

    private void UpdateCanvas(int diameter) {
        theCanvasPane.DrawCircleAtCenterOfCanvas(diameter);
    }

这是我的 Canvas 窗格 class:

public class CanvasPane extends Pane {

private final Canvas theCanvas;
private GraphicsContext theGC;

public CanvasPane(double width, double height) {
    setWidth(width);
    setHeight(height);
    theCanvas = new Canvas(width, height);
    theGC = theCanvas.getGraphicsContext2D();

    getChildren().add(theCanvas);
    theCanvas.widthProperty().bind(this.widthProperty());
    theCanvas.heightProperty().bind(this.heightProperty());
    theCanvas.widthProperty().addListener(observable -> RedrawCanvas());
    theCanvas.heightProperty().addListener(observable -> RedrawCanvas());
}

private void RedrawCanvas() {
    ClearCanvas();
}

private void ClearCanvas() {
    theGC.clearRect(0, 0, theCanvas.widthProperty().doubleValue(),   theCanvas.heightProperty().doubleValue());
}

public void DrawCircleAtCenterOfCanvas(int diameter) {
    double centreX = theCanvas.widthProperty().doubleValue() / 2;
    double centreY = theCanvas.heightProperty().doubleValue() / 2;
    theGC.fillOval(centreX - diameter / 2.0, centreY - diameter / 2.0, diameter, diameter);
}

}

最后,这是我的应用程序 class 和我的 .fxml

public class App extends Application {

    private static Scene scene;

    @Override
    public void start(Stage stage) throws IOException {
        scene = new Scene(loadFXML("primary"));
        stage.setScene(scene);
        //stage.setResizable(false);
        stage.show();
    }

    private static Parent loadFXML(String fxml) throws IOException {
        FXMLLoader fxmlLoader = new FXMLLoader(App.class.getResource(fxml + ".fxml"));
        return fxmlLoader.load();
    }

    public static void main(String[] args) {
        launch();
    }
}

primary.fxml

<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.StackPane?>
<StackPane fx:id="theRootPane" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.xxx.PrimaryController" />

它工作正常,直到我调整 window 的大小,此时,通过清除 canvas 重新绘制它,然后在 canvas 上绘制新的更大的圆圈。当调整表单大小时,canvas 的这种“清除”表现为闪烁。

执行此操作的更好方法是什么?在学习 Java 并进入 UI 和动画之后,我正在使用 JavaFX。我在想 canvas 是 不是 要走的路...

如有任何建议,我们将不胜感激。

ChangeListener 中更新 canvas 可能比在 InvalidationListener 中更新更好,这样可以减少重绘。无论哪种方式,您都应该:

  1. 确保在 canvas 大小更改时重新绘制圆(使用当前代码,一旦 canvas 更改大小,您就清除 canvas,但是在下一个关键帧之前不要重新绘制圆圈,所以你最终会在中间有一些空白 canvases):

     public class CanvasPane extends Pane {
    
         private final Canvas theCanvas;
         private GraphicsContext theGC;
    
         private int currentDiameter ;
    
         public CanvasPane(double width, double height) {
             setWidth(width);
             setHeight(height);
             theCanvas = new Canvas(width, height);
             theGC = theCanvas.getGraphicsContext2D();
    
             getChildren().add(theCanvas);
             theCanvas.widthProperty().bind(this.widthProperty());
             theCanvas.heightProperty().bind(this.heightProperty());
             theCanvas.widthProperty().addListener((obs, oldWidth, newWidth) -> redrawCanvas());
             theCanvas.heightProperty().addListener((obs, oldHeight, newHeight) -> redrawCanvas());
         }
    
         public void increaseDiameter() {
             currentDiameter++;
             redrawCanvas();
         }
    
         private void redrawCanvas() {
             clearCanvas();
             drawCircleAtCenterOfCanvas();
         }
    
         private void clearCanvas() {
             theGC.clearRect(0, 0, theCanvas.widthProperty().doubleValue(), theCanvas.heightProperty().doubleValue());
         }
    
         public void drawCircleAtCenterOfCanvas() {
             currentDiameter = currentDiameter ;
             double centreX = theCanvas.widthProperty().doubleValue() / 2;
             double centreY = theCanvas.heightProperty().doubleValue() / 2;
             theGC.fillOval(centreX - currentDiameter / 2.0, centreY - currentDiameter / 2.0, currentDiameter, currentDiameter);
         }
     }
    

    public class PrimaryController {
    
         @FXML
         private StackPane theRootPane;
         @FXML
         private CanvasPane theCanvasPane;
    
         public void initialize() {
             theCanvasPane  = new CanvasPane(500, 300);
             theRootPane.getChildren().addAll(theCanvasPane);
    
             Timeline theTimeline =
                 new Timeline(new KeyFrame(Duration.millis(50), actionEvent -> updateCanvas()));
             theTimeline.setCycleCount(Timeline.INDEFINITE);
             theTimeline.play();
         }
    
         private void updateCanvas() {
             theCanvasPane.increaseDiameter();
         }
     }
    
  2. 或者(对于这个例子可能更好,也更容易)使用 Circle 而不是 canvas:

     public class PrimaryController {
    
         @FXML
         private StackPane theRootPane;
    
         private Circle circle ;
    
         public void initialize() {
    
             circle = new Circle();
             theRootPane.getChildren().addAll(circle);
    
             Timeline theTimeline =
                 new Timeline(new KeyFrame(Duration.millis(50), actionEvent -> updateCircle()));
             theTimeline.setCycleCount(Timeline.INDEFINITE);
             theTimeline.play();
         }
    
         private void updateCircle() {
             circle.setRadius(circle.getRadius()+0.5);
         }
     }