JavaFX Glitches animate custom widget

I'm trying to prove an extensible widget with JavaFX, which is different TitledPane

in that the border surrounding the label grows with the widget as it expands. I found a way to do this.

However, I have a strange glitch that appears after expanding / dropping once or twice.

This is a link to a short video where the problem occurs with the third and fourth expansion:

Brief Youtube / Glitch Video

I ran out of things that I could try to force myself to behave.

The code looks like this, apologies for its insanity, wanted it to work before refactoring it.

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

      

+3


source to share


1 answer


I could understand what ListView

is the reason that there is a glitch.

Basically, except for the first time, every time you add the list to VBox

, you can see the list in full size for a very short moment outside of its container, and then when the timeline starts, it resized correctly.

You can actually add a delay (like one second) to the timeline:

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

      

and you will see the problem for the whole second if you expand the box a second time:

Glitch

The list is displayed outside VBox

because it does not change to match it. When the animation starts, it changed and the problem went away.

I've tried several approaches to resize the list at this point, with no success. Maybe you can work it out ...

One ugly solution would be to create a new instance of the list every time the window is expanded:

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

      

Looking for other attributes, I have tied the field disableProperty()

to the timeline so you cannot click when the list header expands or shrinks.

So the other solution is linking the list visibleProperty()

to the timeline, but you won't see a nice growing effect.

And there is a third solution that will match the animation as well: set the opacity to 0 right before adding the list to the box:

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

      



and add a new KeyValue

, incrementing list opacityProperty()

from 0 to 1 to the timeline:

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

      

Now you won't see a crash and the list will display well as long as the window is resized. In fact, I will increase the duration of the second keyframe.

List resized

EDIT

I have another alternative to avoid the glitch. It can also improve the animation, since the list will be visible even when the named list is shortened.

When you sign a named list in the first place you delete a list, so super.computePrefHeight(width)

and super.computePrefWidth(height)

get a new small window size. This has the distinct disadvantage that on next expansion the list has to be added again and it crashes.

To avoid this, we will not delete the list. First, we create two new fields on ExpansionManager

:

Double minWidth = 0.0;
Double minHeight = 0.0;

      

We then get the minimum window size (on first expansion) and use that for each cut:

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

      

Finally, we need to hide the list after any shortcut, otherwise there will be a small border and change the methods expand()

and contract()

for the call requestLayout()

, given that the list of window children is no longer changed (except for the first call):

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

      

+1


source







All Articles