JavaFX动画非标准小部件时出现问题

3

我正在尝试使用JavaFX实现一个可扩展的小部件,与TitledPane不同的是,包围标签的边框会随着小部件的扩展而增长。我已经找到了一种方法来做到这一点。

然而,我有一个奇怪的故障,在扩展/折叠一两次后就会出现。

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

短Youtube概念/故障视频

我已经没有更多可以尝试让它正常工作的方法了。

代码如下,对于其奇怪性质我表示歉意,我想在重构之前让它正常工作。

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);
    }
}

我在我的答案中添加了另一种解决方案,与您的方法相似,希望这个能引起您的兴趣。 - José Pereda
1个回答

1
我能理解到问题出在 ListView 上面。基本上,除第一次外,每次你将列表添加到 VBox 中,你都会看到列表在其容器外部以全尺寸显示一个非常短暂的瞬间,然后当时间轴开始时,它会被正确地调整大小。
实际上,你可以在时间轴中添加一个延迟(比如一秒钟):
timeline.setDelay(Duration.millis(1000));

如果您第二次展开框,您将在整个第二秒看到问题。

Glitch

  • 由于列表没有被调整大小以适应VBox的大小,所以列表在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()))
); 

现在你不会看到故障,而且列表将在盒子调整大小时显示得很好。事实上,我将增加第二个关键帧的持续时间。

List resized

编辑

我有另一种避免“故障”的替代方案。此外,它可以改善动画效果,因为列表在标题列表收缩时也可见。

当您收缩标题列表时,首先要删除列表,因此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(),因为盒子子元素列表不再被修改(除了第一次调用)。
 @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();
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接