JavaFX Glitches 动画非标准小部件

Posted

技术标签:

【中文标题】JavaFX Glitches 动画非标准小部件【英文标题】:JavaFX Glitches animating non standard widget 【发布时间】:2015-03-02 11:35:15 【问题描述】:

我试图用 JavaFX 证明一个可扩展的小部件,它与 TitledPane 的不同之处在于标签周围的边框随着小部件的扩展而增长。我找到了一种方法。

但是,我在展开/折叠一两次后出现了一个奇怪的故障。

这是一个短视频的链接,其中问题出现在第三和第四扩展:

Short Youtube Concept / Glitch Video

我已经用完了可以尝试让它正常运行的东西。

代码如下,对它的古怪表示歉意,希望在我重构它之前让它工作。

class ExpansionManager 
    enum LayoutState 
        INITIALIZE,
        ANIMATING,
        IDLE,
        REQUEST_ANIMATION
    

    LayoutState layoutState = LayoutState.INITIALIZE;
    Double fromWidth = 0.0;
    Double fromHeight = 0.0;
    Double stepWidth = 0.0;
    Double stepHeight = 0.0;
    Double toWidth = 0.0;
    Double toHeight = 0.0;


public class ExpandableTitledList extends VBox 
    private Label title = new Label();
    private ListProperty<String> listItem = new SimpleListProperty<>();
    private ListView listView = new ListView<>(listItem);
    Timeline timeline;

    WritableValue<Double> writableHeight = new WritableValue<Double>() 
        @Override
        public Double getValue() 
            return expansionManager.stepHeight;
        

        @Override
        public void setValue(Double value) 
            expansionManager.stepHeight = value;
            requestLayout();
        
    ;

    WritableValue<Double> writableWidth = new WritableValue<Double>() 
        @Override
        public Double getValue() 
            return expansionManager.stepWidth;
        

        @Override
        public void setValue(Double value) 
            expansionManager.stepWidth = value;
            requestLayout();
        
    ;

    private boolean expanded = false;
    ExpansionManager expansionManager = new ExpansionManager();
//    private Dimension2D contractedDimension;
//    private Dimension2D expandedDimension;


    public ExpandableTitledList() 
        setTitle("boom");
//        title.layout();
//        System.out.println(title.getLayoutBounds().getWidth());
        // set down right caret
        listItem.setValue(FXCollections.observableArrayList("one", "two"));

        Insets theInsets = new Insets(-3, -5, -3, -5);
        Border theBorder = new Border(
                new BorderStroke(
                        Color.BLACK,
                        BorderStrokeStyle.SOLID,
                        new CornerRadii(4),
                        new BorderWidths(2),
                        theInsets
                )
        );

//        expandedDimension = new Dimension2D(200,200);

        setBorder(theBorder);
        getChildren().addAll(title);

        title.setOnMouseClicked((event) -> 
            System.out.println("mouse clicked");
            if (this.expanded) contract();
            else expand();
        );
    

    @Override
    protected void layoutChildren() 
        System.out.println(expansionManager.layoutState);
        if (expansionManager.layoutState == ExpansionManager.LayoutState.INITIALIZE) 
            super.layoutChildren();
            expansionManager.layoutState = ExpansionManager.LayoutState.IDLE;
         else if (expansionManager.layoutState == ExpansionManager.LayoutState.ANIMATING) 
            super.layoutChildren();
         else if (expansionManager.layoutState == ExpansionManager.LayoutState.REQUEST_ANIMATION) 
            setCache(false);
            listView.setCache(false);

            expansionManager.layoutState = ExpansionManager.LayoutState.ANIMATING;
            System.out.println("from : " + expansionManager.fromWidth + ", "+ expansionManager.fromHeight);
            System.out.println("to : " + expansionManager.toWidth + ", "+ expansionManager.toHeight);

            timeline = new Timeline();

            timeline.getKeyFrames().addAll(
                    new KeyFrame(Duration.ZERO,
                            new KeyValue(writableHeight, expansionManager.fromHeight),
                            new KeyValue(writableWidth, expansionManager.fromWidth)),
                    new KeyFrame(Duration.millis(100),
                            new KeyValue(writableHeight, expansionManager.toHeight),
                            new KeyValue(writableWidth, expansionManager.toWidth))
            );
            timeline.play();

            timeline.setOnFinished((done) -> 
                System.out.println("done");
                expansionManager.layoutState = ExpansionManager.LayoutState.IDLE;
                timeline = null;
            );

         else 
            System.out.println("idle");
            super.layoutChildren();
        
    

    @Override
    protected double computePrefHeight(double width) 
        if (expansionManager.layoutState == ExpansionManager.LayoutState.INITIALIZE) 
            expansionManager.fromHeight = super.computePrefHeight(width);
            return expansionManager.fromHeight;
         else if (expansionManager.layoutState == ExpansionManager.LayoutState.ANIMATING) 
            return expansionManager.stepHeight;
         else if (expansionManager.layoutState == ExpansionManager.LayoutState.REQUEST_ANIMATION) 
            expansionManager.fromHeight = getHeight();
            expansionManager.stepHeight = expansionManager.fromHeight;
            expansionManager.toHeight = super.computePrefHeight(width);
            return expansionManager.fromHeight;
         else 
            return expansionManager.toHeight;
        
    

    @Override
    protected double computePrefWidth(double height) 
        if (expansionManager.layoutState == ExpansionManager.LayoutState.INITIALIZE) 
            expansionManager.fromWidth = super.computePrefWidth(height);
            return expansionManager.fromWidth;
         else if (expansionManager.layoutState == ExpansionManager.LayoutState.ANIMATING) 
            return expansionManager.stepWidth;
         else if (expansionManager.layoutState == ExpansionManager.LayoutState.REQUEST_ANIMATION) 
            expansionManager.fromWidth = getWidth();
            expansionManager.stepWidth = expansionManager.fromWidth;
            expansionManager.toWidth = super.computePrefWidth(height);
            return expansionManager.fromWidth;
         else 
            System.out.println("BANG BANG BANG");
            return expansionManager.toWidth;
        
    

//    @Override
//    protected double computeMinWidth(double height) 
//        return computePrefWidth(height);
//    
//
//    @Override
//    protected double computeMinHeight(double width) 
//        return computePrefHeight(width);
//    
//
//    @Override
//    protected double computeMaxWidth(double height) 
//        return computePrefWidth(height);
//    
//
//    @Override
//    protected double computeMaxHeight(double width) 
//        return computePrefHeight(width);
//    

    private void expand() 
        System.out.println(expansionManager.layoutState);
//        if(contractedDimension == null)
//            contractedDimension = new Dimension2D(this.getWidth(), this.getHeight());
//        setPrefSize(expandedDimension.getWidth(), expandedDimension.getHeight());
        expansionManager.layoutState = ExpansionManager.LayoutState.REQUEST_ANIMATION;
        this.getChildren().setAll(title, listView);
        expanded = true;

    

    private void contract() 
//        this.setPrefSize(contractedDimension.getWidth(), contractedDimension.getHeight());
        expansionManager.layoutState = ExpansionManager.LayoutState.REQUEST_ANIMATION;
        this.getChildren().setAll(title);
        expanded = false;

    

    public String getTitle() 
        return title.getText();
    

    public void setTitle(String title) 
        this.title.setText(title);
    

【问题讨论】:

我为我的答案添加了另一个解决方案,与您的方法一致,希望您对此感兴趣。 【参考方案1】:

我知道ListView 是出现故障的原因。

基本上,除了第一次,每次您将列表添加到 VBox 时,您都可以在非常短的时间内在其容器外看到完整大小的列表,然后当时间线开始时,它会正确调整大小.

实际上,您可以在时间轴上添加延迟(例如一秒):

timeline.setDelay(Duration.millis(1000));

如果您第二次展开该框,您将看到整整一秒的问题:

该列表在VBox 之外可见,因为它没有调整大小以适应它。当动画开始时,它被调整大小并且问题消失了。

当时我尝试了几种方法来调整列表的大小,但均未成功。也许你可以解决它...

一个丑陋的解决方案是每次展开框时创建一个新的列表实例:

private void expand() 
    expansionManager.layoutState = ExpansionManager.LayoutState.REQUEST_ANIMATION;
    // this works...    
    listView = new ListView<>(listItem);
    this.getChildren().setAll(title,listView);
    expanded = true;

寻找其他替代方案,我已将框 disableProperty() 绑定到时间线,因此在标题列表正在扩展或收缩时您无法单击。

所以另一种解决方案是将列表visibleProperty() 绑定到时间线,但您不会看到很好的增长效果。

还有第三种解决方案,也符合动画:在将列表添加到框之前将不透明度设置为 0:

private void expand() 
    expansionManager.layoutState = ExpansionManager.LayoutState.REQUEST_ANIMATION;
    // this will avoid seeing the unresized listView
    listView.setOpacity(0);
    this.getChildren().setAll(title,listView);
    expanded = true;

并添加一个新的KeyValue 将列表opacityProperty() 从0 增加到1 到时间线:

timeline.getKeyFrames().setAll(
    new KeyFrame(Duration.ZERO,
            new KeyValue(listView.opacityProperty(), 0),
            new KeyValue(writableHeight, expansionManager.fromHeight.get()),
            new KeyValue(writableWidth, expansionManager.fromWidth.get())),
    new KeyFrame(Duration.millis(300),
            new KeyValue(listView.opacityProperty(), 1),
            new KeyValue(writableHeight, expansionManager.toHeight.get()),
            new KeyValue(writableWidth, expansionManager.toWidth.get()))
); 

现在您不会看到故障,并且在调整框大小时列表会很好地显示出来。事实上,我会增加第二个关键帧的持续时间。

编辑

我有另一种选择来避免“故障”。它还可以改善动画,因为当标题列表收缩时,列表也会可见。

当你收缩标题列表时,首先你删除列表,所以super.computePrefHeight(width)super.computePrefWidth(height)得到小框的新大小。这有一个明显的缺点,即在下一次展开时,必须再次添加列表,并且会发生故障。

为了避免这种情况,我们不会删除该列表。首先,我们在ExpansionManager上新建两个字段:

Double minWidth = 0.0;
Double minHeight = 0.0;

然后我们得到盒子的最小尺寸(在第一次扩展时),并在每次收缩时使用它:

@Override
protected double computePrefHeight(double width) 
     ...
     if (expansionManager.layoutState == ExpansionManager.LayoutState.REQUEST_ANIMATION) 
        if(expansionManager.minHeight==0d)
            expansionManager.minHeight=getHeight();
        
        expansionManager.fromHeight = getHeight();
        expansionManager.stepHeight = expansionManager.fromHeight;
        expansionManager.toHeight = expanded?super.computePrefHeight(width):
                                             expansionManager.minHeight;
        return expansionManager.fromHeight;
    


@Override
protected double computePrefWidth(double height) 
    ...
    if (expansionManager.layoutState == ExpansionManager.LayoutState.REQUEST_ANIMATION) 
        if(expansionManager.minWidth==0d)
            expansionManager.minWidth=getWidth();
        
        expansionManager.fromWidth = getWidth();
        expansionManager.stepWidth = expansionManager.fromWidth;
        expansionManager.toWidth = expanded?super.computePrefWidth(height):
                                            expansionManager.minWidth;
        return expansionManager.fromWidth;
    

最后,我们需要在任何收缩后隐藏列表,否则会看到一个小边框,并将expand()contract()方法更改为调用requestLayout(),因为box children列表不再修改(第一次通话除外):

 @Override
protected void layoutChildren() 
        timeline.setOnFinished((done) -> 
            expansionManager.layoutState = ExpansionManager.LayoutState.IDLE;
            listView.setVisible(expanded);
            timeline = null;
        );


private void expand() 
    expansionManager.layoutState = ExpansionManager.LayoutState.REQUEST_ANIMATION;
    expanded = true;
    listView.setVisible(true);
    if(this.getChildren().size()==1)
        this.getChildren().add(listView);
    
    requestLayout();


private void contract() 
    expansionManager.layoutState = ExpansionManager.LayoutState.REQUEST_ANIMATION;
    expanded = false;
    requestLayout();

【讨论】:

以上是关于JavaFX Glitches 动画非标准小部件的主要内容,如果未能解决你的问题,请参考以下文章

iOS 14 小部件中的动画

Flutter - 我可以用 Hero 包装每个小部件来为它们设置动画吗

Flutter:一次多次使用相同的动画小部件

Flutter:同时进行英雄过渡 + 小部件动画?

Flutter-当内容更改时淡入淡出动画文本小部件

Android 小部件:RemoteViews 上的动画? [复制]