在JavaFX中为ListView新增条目时如何添加动画效果

5

问题

你好,

我正在尝试编写一个应用程序,在其中每个新的ListView条目都会得到动画效果。以下是我的代码:

public class BookCell extends ListCell<Book>
{
    private Text text;
    private HBox h;
    
    public BookCell()
    {
        this.text = new Text();
        this.h = new HBox();
        this.h.getChildren().add(text);
        super.getStyleClass().add("book-list-cell");
        super.itemProperty().addListener((obs,oldv,newv)->{
            if(newv != null )
            {
                if(getIndex() == this.getListView().getItems().size()-1 )
                {
                    //why does this get called twice for each update?
                    System.out.println("isbn = "+newv.getIsbn().get() + "   lastIndexOf=" + this.getListView().getItems().lastIndexOf(newv)+"    Index="+getIndex()+"   size="+this.getListView().getItems().size());
                    runAnimation();
                }
            }
            
        });
        this.getChildren().add(h);
    }
    @Override
    protected void updateItem(Book item, boolean empty)
    {
         super.updateItem(item, empty);
         if(!empty)
             super.setGraphic(h);
         text.setText(item == null ? "" : item.getIsbn().get());
         
    }
    
    public void runAnimation()
    {
        FadeTransition f = new FadeTransition();
        f.setFromValue(0);
        f.setToValue(1);
        f.setNode(this);
        f.play();
    }
}

我试图将监听器添加到itemProperty中,但我得到了一些奇怪的行为。首先,每次我添加一个条目时,监听器都会触发两次:

enter image description here

这导致动画播放两次。即使在这种情况下几乎不会注意到,但这可能有点尴尬。

此外,在列表开始滚动后,有时会为可见条目重复执行动画:

enter image description here

老实说,如果一个项目再次变得可见,我并不一定介意重复动画,但现在的情况非常不可靠,哪些单元格会被动画化还很难确定。

我知道单元格是虚拟控制的,在一段时间内会被重用。但我很难找到一种可靠的方法来确定屏幕上是否出现了非空单元格。

问题

  1. 为什么监听器会触发两次?
  2. 观察屏幕上单元格的出现/消失的最佳方法是什么?
  3. 有更好的方法向ListView添加动画吗?

完整代码

这可能会有所帮助,因此这是我的css和main文件。

Main.java

public class Main extends Application 
{
    static int newBookIndex = 1;        
    public static final ObservableList<Book> data =  FXCollections.observableArrayList();
    @Override
    public void start(Stage primaryStage) 
    {
        try 
        {
            GridPane myPane = (GridPane)FXMLLoader.load(getClass().getResource("quotes.fxml"));
            ListView<Book> lv = (ListView<Book>) myPane.getChildren().get(0);   
            Button addButton = (Button) myPane.getChildren().get(1);
            addButton.setText("Add Book");
            addButton.setOnAction((event)->{
            data.add(new Book(String.valueOf(newBookIndex++),"test"));
            });
            lv.setEditable(true);
            data.addAll(
                     new Book("123","Hugo"),
                     new Book("456","Harry Potter")
                );
                  
            lv.setItems(data);
            lv.setCellFactory((param)->return new BookCell());
            Scene myScene = new Scene(myPane);
            myScene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
            primaryStage.setScene(myScene);
            primaryStage.show();
        } 
        catch(Exception e)
        {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) 
    {
        launch(args);
    }
}

application.css (基于 https://github.com/privatejava/javafx-listview-animation)


应用程序css文件(基于 https://github.com/privatejava/javafx-listview-animation)
.book-list-cell:odd{

    -fx-background-image: url('./wooden2.png') ,url('./gloss.png');
    -fx-background-repeat: repeat,no-repeat;
}
.book-list-cell:empty {
     -fx-background-image: url('./gloss.png');
     -fx-background-repeat: repeat;
}

.book-list-cell {
    -fx-font-size:45px;
    -fx-font-family: 'Segoe Script';
}
.book-list-cell:selected,.book-list-cell:selected:hover{
    -fx-border-color:linear-gradient(to bottom,transparent,rgb(22,22,22,1));
    -fx-border-width:2 2 2 10px;
    -fx-effect:innershadow( three-pass-box,#764b0c,30,0.3,0,0);
}
.book-list-cell:hover{
    -fx-border-color:rgb(255,255,255,0.7);
    -fx-border-width:1 1 1 10px;
}

.book-list-cell{
    -fx-background-image: url('./wooden.png') ,url('./gloss.png');
    -fx-background-repeat: repeat,no-repeat;
    -fx-background-position:left top;
    -fx-background-size: auto,100% 40%;
}

只是一句旁注。我之前不知道你可以像你在问题中使用的那样使用标题。谢谢你展示了这个!:D - Joop
1个回答

5

为了找出监听器双重触发的原因,我进行了一些调试,跟踪layoutChildren调用。

长话短说:每次单击添加书籍按钮时,itemProperty()中不是两个而是三个更改。为了找出原因,在监听器和updateItem方法上添加一些打印信息:

itemProperty().addListener((obs,oldv,newv)->{
    System.out.println("change: "+getIndex()+", newv "+(newv != null?newv.getIsbn():"null")+", oldv: "+(oldv != null?oldv.getIsbn():"null"));
}

protected void updateItem(Book item, boolean empty){
        super.updateItem(item, empty);
        System.out.println("update item: "+(item!=null?item.getIsbn():"null"));
}

对于第一本书“123”,这是其追踪记录:
update item: null
change: 0, newv 123, oldv: null
update item: 123
change: -1, newv null, oldv: 123
update item: null
update item: null
change: 0, newv 123, oldv: null
update item: 123

第一个更改按预期发生,因为创建了新的单元格。但一旦创建完成,接下来会调用VirtualFlow.releaseCell(),索引为-1,最后使用VirtualFlow.layoutChildren()中的addLeadingCells()addTrailingCells()重新构建所有单元格。

所以根据设计,这些调用无法避免,这意味着如果您想确保它只被调用一次,动画不应该在监听器中。

动画

我提出了一种解决方案,即获取单元格并运行动画:

addButton.setOnAction((event)->{
    data.add(new Book(String.valueOf(newBookIndex++),"test"));

    VirtualFlow vf=(VirtualFlow)lv.lookup(".virtual-flow");
    BookCell cell = (BookCell)vf.getCell(lv.getItems().size()-1);
    cell.runAnimation();
});

这并不是很明智,因为使用了私有API VirtualFlow

可以通过查找CSS选择器indexed-cells来避免这个问题:

addButton.setOnAction((event)->{
    data.add(new Book(String.valueOf(newBookIndex),"test"));

    lv.lookupAll(".indexed-cell").stream()
            .map(n->(BookCell)n)
            .filter(c->c.getBook().getIsbn().equals(String.valueOf(newBookIndex)))
            .findFirst().ifPresent(BookCell::runAnimation);
    newBookIndex++;
});

请注意,由于我们使用了查找功能,因此按钮的事件处理程序应在舞台显示后添加。
编辑:
OP报告了滚动方面的奇怪问题。我不确定这是否是一个bug,但顶部单元格被动画化了,而不是底部单元格。
为了解决这个问题,我提出了这个解决方法,删除了BookCell动画,并为单元格创建了一个动画,使用VirtualFlow获取单元格并在必要时滚动。
首先,我们尝试查找滚动条是否不可见:我们使用我们的淡入淡出转换。但是如果我们有滚动条,现在我们需要调用show()来滚动到最后一个单元格。在启动淡入淡出转换之前,短暂的PauseTransition可以解决问题。
addButton.setOnAction((event)->{
    addButton.setDisable(true);
    data.add(new Book(String.valueOf(newBookIndex),"test"));

    VirtualFlow vf=(VirtualFlow)lv.lookup(".virtual-flow");
    if(!lv.lookup(".scroll-bar").isVisible()){
        FadeTransition f = new FadeTransition();
        f.setDuration(Duration.seconds(1));
        f.setFromValue(0);
        f.setToValue(1);
        f.setNode(vf.getCell(lv.getItems().size()-1));
        f.setOnFinished(t->{
            newBookIndex++;
            addButton.setDisable(false);                        
        });
        f.play();
    } else {
        PauseTransition p = new PauseTransition(Duration.millis(20));
        p.setOnFinished(e->{
            vf.getCell(lv.getItems().size()-1).setOpacity(0);
            vf.show(lv.getItems().size()-1);
            FadeTransition f = new FadeTransition();
            f.setDuration(Duration.seconds(1));
            f.setFromValue(0);
            f.setToValue(1);
            f.setNode(vf.getCell(lv.getItems().size()-1));
            f.setOnFinished(t->{
                newBookIndex++;
                addButton.setDisable(false);                        
            });
            f.play();
        });
        p.play();
    }
});

更新的代码

最后,这是我使用的所有代码:

public class Main extends Application {

    private int newBookIndex = 2;        
    public final ObservableList<Book> data =  FXCollections.observableArrayList(
            new Book("123","Hugo"), new Book("456","Harry Potter"));
    private final ListView<Book> lv = new ListView<>();   

    @Override
    public void start(Stage primaryStage) {
        Button addButton = new Button("Add Book");

        lv.setCellFactory(param->new BookCell());
        lv.setItems(data);
        Scene myScene = new Scene(new VBox(10,lv,addButton), 200, 200);
        myScene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
        primaryStage.setScene(myScene);
        primaryStage.show();

        addButton.setOnAction((event)->{
            addButton.setDisable(true);
            data.add(new Book(String.valueOf(newBookIndex),"test"));

            VirtualFlow vf=(VirtualFlow)lv.lookup(".virtual-flow");
            if(!lv.lookup(".scroll-bar").isVisible()){
                FadeTransition f = new FadeTransition();
                f.setDuration(Duration.seconds(1));
                f.setFromValue(0);
                f.setToValue(1);
                f.setNode(vf.getCell(lv.getItems().size()-1));
                f.setOnFinished(t->{
                    newBookIndex++;
                    addButton.setDisable(false);                        
                });
                f.play();
            } else {
                PauseTransition p = new PauseTransition(Duration.millis(20));
                p.setOnFinished(e->{
                    vf.getCell(lv.getItems().size()-1).setOpacity(0);
                    vf.show(lv.getItems().size()-1);
                    FadeTransition f = new FadeTransition();
                    f.setDuration(Duration.seconds(1));
                    f.setFromValue(0);
                    f.setToValue(1);
                    f.setNode(vf.getCell(lv.getItems().size()-1));
                    f.setOnFinished(t->{
                        newBookIndex++;
                        addButton.setDisable(false);                        
                    });
                    f.play();
                });
                p.play();
            }
        });
    }

    class BookCell extends ListCell<Book>{
        private final Text text = new Text();
        private final HBox h = new HBox(text);

        {
            getStyleClass().add("book-list-cell");
        }

        @Override
        protected void updateItem(Book item, boolean empty){
            super.updateItem(item, empty);
            if(item!=null && !empty){
                text.setText(item.getIsbn());
                setGraphic(h);
            } else {
                setGraphic(null);
                setText(null);
            }
        }
    }

    class Book {
        private Book(String isbn, String name) {
            this.isbn.set(isbn);
            this.name.set(name);
        }

        private final StringProperty name = new SimpleStringProperty();

        public String getName() {
            return name.get();
        }

        public void setName(String value) {
            name.set(value);
        }

        public StringProperty nameProperty() {
            return name;
        }

        private final StringProperty isbn = new SimpleStringProperty();

        public String getIsbn() {
            return isbn.get();
        }

        public void setIsbn(String value) {
            isbn.set(value);
        }

        public StringProperty isbnProperty() {
            return isbn;
        }

    }

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

}

那是一个非常难解决的问题。经过多次尝试,我想出了一个可行的解决方法。你可以试一下,然后告诉我结果。 - José Pereda
是的,这篇文章是我的起点。但是当我运行他的示例(可在Github上获取)时,我的单元格翻转得到处都是,不像视频中那样。它似乎有问题,至少在Java 8中是如此。 - ShrimpPhaser
我尝试了这种方法。但是在滚动条出现之前,对于我的单元格没有显示任何动画。在滚动条出现后,它就可以正常工作了。 - Ivantha
我也有同样的问题@Ivantha!不确定问题是什么。 - Blake Ordway
@Ivantha 这个回答已经有些年头了,如果您想进行跟进评论,请发布一个新问题,并参考这个问题。 - José Pereda
显示剩余5条评论

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